From d76f26aa5e67252780dbcaaadb555fd29f464af4 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 21 May 2025 16:07:51 +0100 Subject: [PATCH 001/250] Make default sketch canvas ID autoincrement --- src/core/main.js | 1 + src/core/p5.Renderer2D.js | 6 +++--- src/webgl/p5.RendererGL.js | 5 ++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/core/main.js b/src/core/main.js index a5c9a6c93d..82eb0b5e5c 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -35,6 +35,7 @@ class p5 { // This is a pointer to our global mode p5 instance, if we're in // global mode. static instance = null; + static sketchCount = 0; static lifecycleHooks = { presetup: [], postsetup: [], diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 23f74bec4b..d3517da0a4 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -10,9 +10,7 @@ import FilterRenderer2D from '../image/filterRenderer2D'; import { Matrix } from '../math/p5.Matrix'; import { PrimitiveToPath2DConverter } from '../shape/custom_shapes'; - const styleEmpty = 'rgba(0,0,0,0)'; -// const alphaThreshold = 0.00125; // minimum visible class Renderer2D extends Renderer { constructor(pInst, w, h, isMainCanvas, elt, attributes = {}) { @@ -29,7 +27,9 @@ class Renderer2D extends Renderer { this.canvas.style.display = 'none'; } - this.elt.id = 'defaultCanvas0'; + if(!this.elt.id){ + this.elt.id = `defaultCanvas${p5.sketchCount++}`; + } this.elt.classList.add('p5Canvas'); // Extend renderer with methods of p5.Element with getters diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 5e46d2d106..a41455f3ed 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -136,7 +136,10 @@ class RendererGL extends Renderer { // hide if offscreen buffer by default this.canvas.style.display = "none"; } - this.elt.id = "defaultCanvas0"; + + if(!this.elt.id){ + this.elt.id = `defaultCanvas${p5.sketchCount++}`; + } this.elt.classList.add("p5Canvas"); // Set and return p5.Element From e9c9d96c3166409a9c4193f3d36d1a4487a5268c Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Fri, 14 Nov 2025 20:03:03 +0000 Subject: [PATCH 002/250] Implement pad zero proof of concept --- src/math/index.js | 2 ++ src/math/p5.Vector.js | 22 +++++++++++++++++----- src/math/patch-vector.js | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 src/math/patch-vector.js diff --git a/src/math/index.js b/src/math/index.js index 2acb397b21..40d8d75924 100644 --- a/src/math/index.js +++ b/src/math/index.js @@ -4,6 +4,7 @@ import random from './random.js'; import trigonometry from './trigonometry.js'; import math from './math.js'; import vector from './p5.Vector.js'; +import patchVector from './patch-vector.js'; export default function(p5){ p5.registerAddon(calculation); @@ -12,4 +13,5 @@ export default function(p5){ p5.registerAddon(trigonometry); p5.registerAddon(math); p5.registerAddon(vector); + p5.registerAddon(patchVector); } diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index a264fb811e..b0b29b9519 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -30,6 +30,8 @@ const calculateRemainder3D = function (xComponent, yComponent, zComponent) { }; class Vector { + _values = []; + // This is how it comes in with createVector() // This check if the first argument is a function constructor(...args) { @@ -521,6 +523,7 @@ class Vector { args = args[0]; } args.forEach((value, index) => { + if(!this._values[index]) this._values[index] = 0; this._values[index] = (this._values[index] || 0) + (value || 0); }); return this; @@ -833,6 +836,7 @@ class Vector { sub(...args) { if (args[0] instanceof Vector) { args[0].values.forEach((value, index) => { + if(!this._values[index]) this._values[index] = 0; this._values[index] -= value || 0; }); } else if (Array.isArray(args[0])) { @@ -1046,6 +1050,7 @@ class Vector { const maxLen = Math.min(this._values.length, v.values.length); for (let i = 0; i < maxLen; i++) { if (Number.isFinite(v.values[i]) && typeof v.values[i] === 'number') { + if(!this._values[i]) this._values[i] = 0; this._values[i] *= v.values[i]; } else { console.warn( @@ -1277,18 +1282,23 @@ class Vector { */ div(...args) { if (args.length === 0) return this; + // If passed a vector if (args.length === 1 && args[0] instanceof Vector) { const v = args[0]; if ( - v._values.every( + v.values.every( val => Number.isFinite(val) && typeof val === 'number' ) ) { - if (v._values.some(val => val === 0)) { + if (v.values.some(val => val === 0)) { console.warn('p5.Vector.prototype.div:', 'divide by 0'); return this; } - this._values = this._values.map((val, i) => val / v._values[i]); + // this._values = this._values.map((val, i) => val / v.values[i]); + for (let i = 0; i < v.values.length; i++) { + if(!this._values[i]) this._values[i] = 0; + this._values[i] /= v.values[i]; + } } else { console.warn( 'p5.Vector.prototype.div:', @@ -1298,6 +1308,7 @@ class Vector { return this; } + // If passed an array if (args.length === 1 && Array.isArray(args[0])) { const arr = args[0]; if (arr.every(val => Number.isFinite(val) && typeof val === 'number')) { @@ -1315,6 +1326,7 @@ class Vector { return this; } + // If passed individual arguments if (args.every(val => Number.isFinite(val) && typeof val === 'number')) { if (args.some(val => val === 0)) { console.warn('p5.Vector.prototype.div:', 'divide by 0'); @@ -1514,7 +1526,7 @@ class Vector { */ dot(...args) { if (args[0] instanceof Vector) { - return this.dot(...args[0]._values); + return this.dot(...args[0].values); } return this._values.reduce((sum, component, index) => { return sum + component * (args[index] || 0); @@ -3034,7 +3046,7 @@ class Vector { equals(...args) { let values; if (args[0] instanceof Vector) { - values = args[0]._values; + values = args[0].values; } else if (Array.isArray(args[0])) { values = args[0]; } else { diff --git a/src/math/patch-vector.js b/src/math/patch-vector.js new file mode 100644 index 0000000000..ec2ed993e8 --- /dev/null +++ b/src/math/patch-vector.js @@ -0,0 +1,14 @@ +export default function patchVector(p5, fn, lifecycles){ + p5.decorateHelper('createVector', function(target){ + return function(...args){ + if(args.length === 0){ + // console.log('empty call to createVector'); + return new Proxy(new p5.Vector(0, 0, 0), { + + }); + }else{ + return target.call(this, ...args); + } + }; + }); +} From 124a60e5fa233197a72191574c64bdb3cc1ed914 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Mon, 1 Dec 2025 11:24:15 +0000 Subject: [PATCH 003/250] Vector use values property directly and calculate dimension --- src/math/p5.Vector.js | 181 ++++++++++++++++++++---------------------- 1 file changed, 88 insertions(+), 93 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index b0b29b9519..8f7f23710a 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -30,62 +30,58 @@ const calculateRemainder3D = function (xComponent, yComponent, zComponent) { }; class Vector { - _values = []; + values = []; // This is how it comes in with createVector() // This check if the first argument is a function constructor(...args) { - let values = args; // .map(arg => arg || 0); if (typeof args[0] === 'function') { this.isPInst = true; this._fromRadians = args[0]; this._toRadians = args[1]; - values = args.slice(2); // .map(arg => arg || 0); - } - let dimensions = values.length; // TODO: make default 3 if no arguments - if (dimensions === 0) { - this.dimensions = 2; - this._values = [0, 0, 0]; - } else { - this.dimensions = dimensions; - this._values = values; - } - } - - /** - * Gets the values of the N-dimensional vector. - * - * This method returns an array of numbers that represent the vector. - * Each number in the array corresponds to a different component of the vector, - * like its position in different directions (e.g., x, y, z). - * - * @returns {Array} The array of values representing the vector. - */ - get values() { - return this._values; - } - - /** - * Sets the values of the vector. - * - * This method allows you to update the entire vector with a new set of values. - * You need to provide an array of numbers, where each number represents a component - * of the vector (e.g., x, y, z). The length of the array should match the number of - * dimensions of the vector. If the array is shorter, the missing components will be - * set to 0. If the array is longer, the extra values will be ignored. - * - * @param {Array} newValues - An array of numbers representing the new values for the vector. - * - */ - set values(newValues) { - let dimensions = newValues.length; - if (dimensions === 0) { - this.dimensions = 2; - this._values = [0, 0, 0]; - } else { - this.dimensions = dimensions; - this._values = newValues.slice(); + args = args.slice(2); } + this.values = args; + } + + // /** + // * Gets the values of the N-dimensional vector. + // * + // * This method returns an array of numbers that represent the vector. + // * Each number in the array corresponds to a different component of the vector, + // * like its position in different directions (e.g., x, y, z). + // * + // * @returns {Array} The array of values representing the vector. + // */ + // get values() { + // return this._values; + // } + + // /** + // * Sets the values of the vector. + // * + // * This method allows you to update the entire vector with a new set of values. + // * You need to provide an array of numbers, where each number represents a component + // * of the vector (e.g., x, y, z). The length of the array should match the number of + // * dimensions of the vector. If the array is shorter, the missing components will be + // * set to 0. If the array is longer, the extra values will be ignored. + // * + // * @param {Array} newValues - An array of numbers representing the new values for the vector. + // * + // */ + // set values(newValues) { + // let dimensions = newValues.length; + // if (dimensions === 0) { + // this.dimensions = 2; + // this._values = [0, 0, 0]; + // } else { + // this.dimensions = dimensions; + // this._values = newValues.slice(); + // } + // } + + get dimensions(){ + return this.values.length; } /** @@ -98,7 +94,7 @@ class Vector { * @returns {Number} The x component of the vector. Returns 0 if the value is not defined. */ get x() { - return this._values[0] || 0; + return this.values[0] || 0; } /** @@ -119,8 +115,8 @@ class Vector { * get a value from a position that doesn't exist in the vector. */ getValue(index) { - if (index < this._values.length) { - return this._values[index]; + if (index < this.values.length) { + return this.values[index]; } else { p5._friendlyError( 'The index parameter is trying to set a value outside the bounds of the vector', @@ -144,8 +140,8 @@ class Vector { * @throws Will throw an error if the index is outside the bounds of the vector, meaning if you try to set a value at a position that doesn't exist in the vector. */ setValue(index, value) { - if (index < this._values.length) { - this._values[index] = value; + if (index < this.values.length) { + this.values[index] = value; } else { p5._friendlyError( 'The index parameter is trying to set a value outside the bounds of the vector', @@ -164,7 +160,7 @@ class Vector { * @returns {Number} The y component of the vector. Returns 0 if the value is not defined. */ get y() { - return this._values[1] || 0; + return this.values[1] || 0; } /** @@ -177,7 +173,7 @@ class Vector { * @returns {Number} The z component of the vector. Returns 0 if the value is not defined. */ get z() { - return this._values[2] || 0; + return this.values[2] || 0; } /** @@ -190,7 +186,7 @@ class Vector { * @returns {Number} The w component of the vector. Returns 0 if the value is not defined. */ get w() { - return this._values[3] || 0; + return this.values[3] || 0; } /** @@ -203,8 +199,8 @@ class Vector { * @param {Number} xVal - The new value for the x component. */ set x(xVal) { - if (this._values.length > 1) { - this._values[0] = xVal; + if (this.values.length > 1) { + this.values[0] = xVal; } } @@ -218,8 +214,8 @@ class Vector { * @param {Number} yVal - The new value for the y component. */ set y(yVal) { - if (this._values.length > 1) { - this._values[1] = yVal; + if (this.values.length > 1) { + this.values[1] = yVal; } } @@ -233,8 +229,8 @@ class Vector { * @param {Number} zVal - The new value for the z component. */ set z(zVal) { - if (this._values.length > 2) { - this._values[2] = zVal; + if (this.values.length > 2) { + this.values[2] = zVal; } } @@ -248,8 +244,8 @@ class Vector { * @param {Number} wVal - The new value for the w component. */ set w(wVal) { - if (this._values.length > 3) { - this._values[3] = wVal; + if (this.values.length > 3) { + this.values[3] = wVal; } } @@ -274,7 +270,7 @@ class Vector { * */ toString() { - return `vector[${this._values.join(', ')}]`; + return `vector[${this.values.join(', ')}]`; } /** @@ -336,13 +332,12 @@ class Vector { */ set(...args) { if (args[0] instanceof Vector) { - this._values = args[0].values.slice(); + this.values = args[0].values.slice(); } else if (Array.isArray(args[0])) { - this._values = args[0].map(arg => arg || 0); + this.values = args[0].map(arg => arg || 0); } else { - this._values = args.map(arg => arg || 0); + this.values = args.map(arg => arg || 0); } - this.dimensions = this._values.length; return this; } @@ -376,9 +371,9 @@ class Vector { */ copy() { if (this.isPInst) { - return new Vector(this._fromRadians, this._toRadians, ...this._values); + return new Vector(this._fromRadians, this._toRadians, ...this.values); } else { - return new Vector(...this._values); + return new Vector(...this.values); } } @@ -523,8 +518,8 @@ class Vector { args = args[0]; } args.forEach((value, index) => { - if(!this._values[index]) this._values[index] = 0; - this._values[index] = (this._values[index] || 0) + (value || 0); + if(!this.values[index]) this.values[index] = 0; + this.values[index] = (this.values[index] || 0) + (value || 0); }); return this; } @@ -836,16 +831,16 @@ class Vector { sub(...args) { if (args[0] instanceof Vector) { args[0].values.forEach((value, index) => { - if(!this._values[index]) this._values[index] = 0; - this._values[index] -= value || 0; + if(!this.values[index]) this.values[index] = 0; + this.values[index] -= value || 0; }); } else if (Array.isArray(args[0])) { args[0].forEach((value, index) => { - this._values[index] -= value || 0; + this.values[index] -= value || 0; }); } else { args.forEach((value, index) => { - this._values[index] -= value || 0; + this.values[index] -= value || 0; }); } return this; @@ -1047,11 +1042,11 @@ class Vector { mult(...args) { if (args.length === 1 && args[0] instanceof Vector) { const v = args[0]; - const maxLen = Math.min(this._values.length, v.values.length); + const maxLen = Math.min(this.values.length, v.values.length); for (let i = 0; i < maxLen; i++) { if (Number.isFinite(v.values[i]) && typeof v.values[i] === 'number') { - if(!this._values[i]) this._values[i] = 0; - this._values[i] *= v.values[i]; + if(!this.values[i]) this.values[i] = 0; + this.values[i] *= v.values[i]; } else { console.warn( 'p5.Vector.prototype.mult:', @@ -1062,10 +1057,10 @@ class Vector { } } else if (args.length === 1 && Array.isArray(args[0])) { const arr = args[0]; - const maxLen = Math.min(this._values.length, arr.length); + const maxLen = Math.min(this.values.length, arr.length); for (let i = 0; i < maxLen; i++) { if (Number.isFinite(arr[i]) && typeof arr[i] === 'number') { - this._values[i] *= arr[i]; + this.values[i] *= arr[i]; } else { console.warn( 'p5.Vector.prototype.mult:', @@ -1079,8 +1074,8 @@ class Vector { typeof args[0] === 'number' && Number.isFinite(args[0]) ) { - for (let i = 0; i < this._values.length; i++) { - this._values[i] *= args[0]; + for (let i = 0; i < this.values.length; i++) { + this.values[i] *= args[0]; } } return this; @@ -1296,8 +1291,8 @@ class Vector { } // this._values = this._values.map((val, i) => val / v.values[i]); for (let i = 0; i < v.values.length; i++) { - if(!this._values[i]) this._values[i] = 0; - this._values[i] /= v.values[i]; + if(!this.values[i]) this.values[i] = 0; + this.values[i] /= v.values[i]; } } else { console.warn( @@ -1316,7 +1311,7 @@ class Vector { console.warn('p5.Vector.prototype.div:', 'divide by 0'); return this; } - this._values = this._values.map((val, i) => val / arr[i]); + this.values = this.values.map((val, i) => val / arr[i]); } else { console.warn( 'p5.Vector.prototype.div:', @@ -1332,7 +1327,7 @@ class Vector { console.warn('p5.Vector.prototype.div:', 'divide by 0'); return this; } - this._values = this._values.map((val, i) => val / args[0]); + this.values = this.values.map((val, i) => val / args[0]); } else { console.warn( 'p5.Vector.prototype.div:', @@ -1414,7 +1409,7 @@ class Vector { * */ magSq() { - return this._values.reduce( + return this.values.reduce( (sum, component) => sum + component * component, 0 ); @@ -1528,7 +1523,7 @@ class Vector { if (args[0] instanceof Vector) { return this.dot(...args[0].values); } - return this._values.reduce((sum, component, index) => { + return this.values.reduce((sum, component, index) => { return sum + component * (args[index] || 0); }, 0); } @@ -3053,8 +3048,8 @@ class Vector { values = args; } - for (let i = 0; i < this._values.length; i++) { - if (this._values[i] !== (values[i] || 0)) { + for (let i = 0; i < this.values.length; i++) { + if (this.values[i] !== (values[i] || 0)) { return false; } } @@ -3074,8 +3069,8 @@ class Vector { * @chainable */ clampToZero() { - for (let i = 0; i < this._values.length; i++) { - this._values[i] = this._clampToZero(this._values[i]); + for (let i = 0; i < this.values.length; i++) { + this.values[i] = this._clampToZero(this.values[i]); } return this; } From 04a2f240f9380861cf2be60b866319760bfab25a Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Mon, 1 Dec 2025 14:21:29 +0000 Subject: [PATCH 004/250] Implement vector smaller size priority --- src/math/p5.Vector.js | 364 +++++++++++++++++++++++------------------- 1 file changed, 204 insertions(+), 160 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 8f7f23710a..49e56cc401 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -517,10 +517,13 @@ class Vector { } else if (Array.isArray(args[0])) { args = args[0]; } - args.forEach((value, index) => { - if(!this.values[index]) this.values[index] = 0; - this.values[index] = (this.values[index] || 0) + (value || 0); - }); + + const resultDimension = Math.min(args.length, this.dimensions); + this.values = this.values.reduce((acc, v, i) => { + if(i < resultDimension) acc[i] = this.values[i] + args[i]; + return acc; + }, new Array(resultDimension)); + return this; } @@ -644,59 +647,74 @@ class Vector { * @param {p5.Vector | Number[]} value divisor vector. * @chainable */ - rem(x, y, z) { - if (x instanceof Vector) { - if ([x.x, x.y, x.z].every(Number.isFinite)) { - const xComponent = parseFloat(x.x); - const yComponent = parseFloat(x.y); - const zComponent = parseFloat(x.z); - return calculateRemainder3D.call( - this, - xComponent, - yComponent, - zComponent - ); - } - } else if (Array.isArray(x)) { - if (x.every(element => Number.isFinite(element))) { - if (x.length === 2) { - return calculateRemainder2D.call(this, x[0], x[1]); - } - if (x.length === 3) { - return calculateRemainder3D.call(this, x[0], x[1], x[2]); - } - } - } else if (arguments.length === 1) { - if (Number.isFinite(arguments[0]) && arguments[0] !== 0) { - this.x = this.x % arguments[0]; - this.y = this.y % arguments[0]; - this.z = this.z % arguments[0]; - return this; - } - } else if (arguments.length === 2) { - const vectorComponents = [...arguments]; - if (vectorComponents.every(element => Number.isFinite(element))) { - if (vectorComponents.length === 2) { - return calculateRemainder2D.call( - this, - vectorComponents[0], - vectorComponents[1] - ); - } - } - } else if (arguments.length === 3) { - const vectorComponents = [...arguments]; - if (vectorComponents.every(element => Number.isFinite(element))) { - if (vectorComponents.length === 3) { - return calculateRemainder3D.call( - this, - vectorComponents[0], - vectorComponents[1], - vectorComponents[2] - ); - } - } + rem(...args) { + if (args[0] instanceof Vector) { + args = args[0].values; + } else if (Array.isArray(args[0])) { + args = args[0]; } + + if(!args.every(v => v !== 0 && Number.isFinite(v))) return this; + + const resultDimension = Math.min(args.length, this.dimensions); + this.values = this.values.reduce((acc, v, i) => { + if(i < resultDimension) acc[i] = this.values[i] % args[i]; + return acc; + }, new Array(resultDimension)); + + return this; + // if (x instanceof Vector) { + // if ([x.x, x.y, x.z].every(Number.isFinite)) { + // const xComponent = parseFloat(x.x); + // const yComponent = parseFloat(x.y); + // const zComponent = parseFloat(x.z); + // return calculateRemainder3D.call( + // this, + // xComponent, + // yComponent, + // zComponent + // ); + // } + // } else if (Array.isArray(x)) { + // if (x.every(element => Number.isFinite(element))) { + // if (x.length === 2) { + // return calculateRemainder2D.call(this, x[0], x[1]); + // } + // if (x.length === 3) { + // return calculateRemainder3D.call(this, x[0], x[1], x[2]); + // } + // } + // } else if (arguments.length === 1) { + // if (Number.isFinite(arguments[0]) && arguments[0] !== 0) { + // this.x = this.x % arguments[0]; + // this.y = this.y % arguments[0]; + // this.z = this.z % arguments[0]; + // return this; + // } + // } else if (arguments.length === 2) { + // const vectorComponents = [...arguments]; + // if (vectorComponents.every(element => Number.isFinite(element))) { + // if (vectorComponents.length === 2) { + // return calculateRemainder2D.call( + // this, + // vectorComponents[0], + // vectorComponents[1] + // ); + // } + // } + // } else if (arguments.length === 3) { + // const vectorComponents = [...arguments]; + // if (vectorComponents.every(element => Number.isFinite(element))) { + // if (vectorComponents.length === 3) { + // return calculateRemainder3D.call( + // this, + // vectorComponents[0], + // vectorComponents[1], + // vectorComponents[2] + // ); + // } + // } + // } } /** @@ -830,19 +848,17 @@ class Vector { */ sub(...args) { if (args[0] instanceof Vector) { - args[0].values.forEach((value, index) => { - if(!this.values[index]) this.values[index] = 0; - this.values[index] -= value || 0; - }); + args = args[0].values; } else if (Array.isArray(args[0])) { - args[0].forEach((value, index) => { - this.values[index] -= value || 0; - }); - } else { - args.forEach((value, index) => { - this.values[index] -= value || 0; - }); + args = args[0]; } + + const resultDimension = Math.min(args.length, this.dimensions); + this.values = this.values.reduce((acc, v, i) => { + if(i < resultDimension) acc[i] = this.values[i] - args[i]; + return acc; + }, new Array(resultDimension)); + return this; } @@ -1040,44 +1056,58 @@ class Vector { * @chainable */ mult(...args) { - if (args.length === 1 && args[0] instanceof Vector) { - const v = args[0]; - const maxLen = Math.min(this.values.length, v.values.length); - for (let i = 0; i < maxLen; i++) { - if (Number.isFinite(v.values[i]) && typeof v.values[i] === 'number') { - if(!this.values[i]) this.values[i] = 0; - this.values[i] *= v.values[i]; - } else { - console.warn( - 'p5.Vector.prototype.mult:', - 'v contains components that are either undefined or not finite numbers' - ); - return this; - } - } - } else if (args.length === 1 && Array.isArray(args[0])) { - const arr = args[0]; - const maxLen = Math.min(this.values.length, arr.length); - for (let i = 0; i < maxLen; i++) { - if (Number.isFinite(arr[i]) && typeof arr[i] === 'number') { - this.values[i] *= arr[i]; - } else { - console.warn( - 'p5.Vector.prototype.mult:', - 'arr contains elements that are either undefined or not finite numbers' - ); - return this; - } - } - } else if ( - args.length === 1 && - typeof args[0] === 'number' && - Number.isFinite(args[0]) - ) { - for (let i = 0; i < this.values.length; i++) { - this.values[i] *= args[0]; - } + if (args[0] instanceof Vector) { + args = args[0].values; + } else if (Array.isArray(args[0])) { + args = args[0]; } + + if(!args.every(v => v !== 0 && Number.isFinite(v))) return this; + + const resultDimension = Math.min(args.length, this.dimensions); + this.values = this.values.reduce((acc, v, i) => { + if(i < resultDimension) acc[i] = this.values[i] * args[i]; + return acc; + }, new Array(resultDimension)); + + // if (args.length === 1 && args[0] instanceof Vector) { + // const v = args[0]; + // const maxLen = Math.min(this.values.length, v.values.length); + // for (let i = 0; i < maxLen; i++) { + // if (Number.isFinite(v.values[i]) && typeof v.values[i] === 'number') { + // if(!this.values[i]) this.values[i] = 0; + // this.values[i] *= v.values[i]; + // } else { + // console.warn( + // 'p5.Vector.prototype.mult:', + // 'v contains components that are either undefined or not finite numbers' + // ); + // return this; + // } + // } + // } else if (args.length === 1 && Array.isArray(args[0])) { + // const arr = args[0]; + // const maxLen = Math.min(this.values.length, arr.length); + // for (let i = 0; i < maxLen; i++) { + // if (Number.isFinite(arr[i]) && typeof arr[i] === 'number') { + // this.values[i] *= arr[i]; + // } else { + // console.warn( + // 'p5.Vector.prototype.mult:', + // 'arr contains elements that are either undefined or not finite numbers' + // ); + // return this; + // } + // } + // } else if ( + // args.length === 1 && + // typeof args[0] === 'number' && + // Number.isFinite(args[0]) + // ) { + // for (let i = 0; i < this.values.length; i++) { + // this.values[i] *= args[0]; + // } + // } return this; } @@ -1276,64 +1306,78 @@ class Vector { * @chainable */ div(...args) { - if (args.length === 0) return this; - // If passed a vector - if (args.length === 1 && args[0] instanceof Vector) { - const v = args[0]; - if ( - v.values.every( - val => Number.isFinite(val) && typeof val === 'number' - ) - ) { - if (v.values.some(val => val === 0)) { - console.warn('p5.Vector.prototype.div:', 'divide by 0'); - return this; - } - // this._values = this._values.map((val, i) => val / v.values[i]); - for (let i = 0; i < v.values.length; i++) { - if(!this.values[i]) this.values[i] = 0; - this.values[i] /= v.values[i]; - } - } else { - console.warn( - 'p5.Vector.prototype.div:', - 'vector contains components that are either undefined or not finite numbers' - ); - } - return this; - } - - // If passed an array - if (args.length === 1 && Array.isArray(args[0])) { - const arr = args[0]; - if (arr.every(val => Number.isFinite(val) && typeof val === 'number')) { - if (arr.some(val => val === 0)) { - console.warn('p5.Vector.prototype.div:', 'divide by 0'); - return this; - } - this.values = this.values.map((val, i) => val / arr[i]); - } else { - console.warn( - 'p5.Vector.prototype.div:', - 'array contains components that are either undefined or not finite numbers' - ); - } - return this; + if (args[0] instanceof Vector) { + args = args[0].values; + } else if (Array.isArray(args[0])) { + args = args[0]; } - // If passed individual arguments - if (args.every(val => Number.isFinite(val) && typeof val === 'number')) { - if (args.some(val => val === 0)) { - console.warn('p5.Vector.prototype.div:', 'divide by 0'); - return this; - } - this.values = this.values.map((val, i) => val / args[0]); - } else { - console.warn( - 'p5.Vector.prototype.div:', - 'arguments contain components that are either undefined or not finite numbers' - ); - } + if(!args.every(v => v !== 0 && Number.isFinite(v))) return this; + + const resultDimension = Math.min(args.length, this.dimensions); + this.values = this.values.reduce((acc, v, i) => { + if(i < resultDimension) acc[i] = this.values[i] / args[i]; + return acc; + }, new Array(resultDimension)); + + // if (args.length === 0) return this; + // // If passed a vector + // if (args.length === 1 && args[0] instanceof Vector) { + // const v = args[0]; + // if ( + // v.values.every( + // val => Number.isFinite(val) && typeof val === 'number' + // ) + // ) { + // if (v.values.some(val => val === 0)) { + // console.warn('p5.Vector.prototype.div:', 'divide by 0'); + // return this; + // } + // // this._values = this._values.map((val, i) => val / v.values[i]); + // for (let i = 0; i < v.values.length; i++) { + // if(!this.values[i]) this.values[i] = 0; + // this.values[i] /= v.values[i]; + // } + // } else { + // console.warn( + // 'p5.Vector.prototype.div:', + // 'vector contains components that are either undefined or not finite numbers' + // ); + // } + // return this; + // } + + // // If passed an array + // if (args.length === 1 && Array.isArray(args[0])) { + // const arr = args[0]; + // if (arr.every(val => Number.isFinite(val) && typeof val === 'number')) { + // if (arr.some(val => val === 0)) { + // console.warn('p5.Vector.prototype.div:', 'divide by 0'); + // return this; + // } + // this.values = this.values.map((val, i) => val / arr[i]); + // } else { + // console.warn( + // 'p5.Vector.prototype.div:', + // 'array contains components that are either undefined or not finite numbers' + // ); + // } + // return this; + // } + + // // If passed individual arguments + // if (args.every(val => Number.isFinite(val) && typeof val === 'number')) { + // if (args.some(val => val === 0)) { + // console.warn('p5.Vector.prototype.div:', 'divide by 0'); + // return this; + // } + // this.values = this.values.map((val, i) => val / args[0]); + // } else { + // console.warn( + // 'p5.Vector.prototype.div:', + // 'arguments contain components that are either undefined or not finite numbers' + // ); + // } return this; } From 1be7b20bc5c0402ec98b5ab1cd731619e05b1c98 Mon Sep 17 00:00:00 2001 From: dhowe Date: Mon, 8 Dec 2025 21:31:31 +0000 Subject: [PATCH 005/250] implemented first 10 getters --- src/color/setting.js | 8 +++----- src/core/p5.Renderer.js | 17 +++++++++++------ src/core/p5.Renderer2D.js | 27 ++++++++++++++++++++++++++- src/image/loading_displaying.js | 6 ++++++ src/shape/attributes.js | 15 +++++++++++++-- 5 files changed, 59 insertions(+), 14 deletions(-) diff --git a/src/color/setting.js b/src/color/setting.js index e60af75c3f..7e1d92b995 100644 --- a/src/color/setting.js +++ b/src/color/setting.js @@ -1470,8 +1470,7 @@ function setting(p5, fn){ * @chainable */ fn.fill = function(...args) { - this._renderer.fill(...args); - return this; + return this._renderer.fill(...args); }; /** @@ -1839,8 +1838,7 @@ function setting(p5, fn){ * @chainable */ fn.stroke = function(...args) { - this._renderer.stroke(...args); - return this; + return this._renderer.stroke(...args); }; /** @@ -2443,7 +2441,7 @@ function setting(p5, fn){ ); mode = constants.BLEND; } - this._renderer.blendMode(mode); + return this._renderer.blendMode(mode); }; } diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 75d01bc04c..f2d60f4dac 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -323,9 +323,12 @@ class Renderer { } fill(...args) { - this.states.setValue('fillSet', true); - this.states.setValue('fillColor', this._pInst.color(...args)); - this.updateShapeVertexProperties(); + if (args.length > 0) { + this.states.setValue('fillSet', true); + this.states.setValue('fillColor', this._pInst.color(...args)); + this.updateShapeVertexProperties(); + } + return this.states.fillColor; } noFill() { @@ -333,14 +336,16 @@ class Renderer { } strokeWeight(w) { - if (w === undefined) { + if (typeof w === 'undefined') { return this.states.strokeWeight; - } else { - this.states.setValue('strokeWeight', w); } + this.states.setValue('strokeWeight', w); } stroke(...args) { + if (args.length === 0) { + return this.states.strokeColor; + } this.states.setValue('strokeSet', true); this.states.setValue('strokeColor', this._pInst.color(...args)); this.updateShapeVertexProperties(); diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index b9a7e12d00..635218080f 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -213,6 +213,9 @@ class Renderer2D extends Renderer { fill(...args) { super.fill(...args); const color = this.states.fillColor; + if (args.length === 0) { + return color; // getter + } this._setFill(color.toString()); // Add accessible outputs if the method exists; on success, @@ -225,6 +228,9 @@ class Renderer2D extends Renderer { stroke(...args) { super.stroke(...args); const color = this.states.strokeColor; + if (args.length === 0) { + return color; // getter + } this._setStroke(color.toString()); // Add accessible outputs if the method exists; on success, @@ -475,6 +481,9 @@ class Renderer2D extends Renderer { ////////////////////////////////////////////// blendMode(mode) { + if (typeof mode === 'undefined') { // getter + return this._cachedBlendMode; + } if (mode === constants.SUBTRACT) { console.warn('blendMode(SUBTRACT) only works in WEBGL mode.'); } else if ( @@ -921,6 +930,9 @@ class Renderer2D extends Renderer { ////////////////////////////////////////////// strokeCap(cap) { + if (typeof cap === 'undefined') { // getter + return this.drawingContext.lineCap; + } if ( cap === constants.ROUND || cap === constants.SQUARE || @@ -932,6 +944,9 @@ class Renderer2D extends Renderer { } strokeJoin(join) { + if (typeof join === 'undefined') { // getter + return this.drawingContext.lineJoin; + } if ( join === constants.ROUND || join === constants.BEVEL || @@ -944,7 +959,10 @@ class Renderer2D extends Renderer { strokeWeight(w) { super.strokeWeight(w); - if (typeof w === 'undefined' || w === 0) { + if (typeof w === 'undefined') { + return this.states.strokeWeight; + } + if (w === 0) { // hack because lineWidth 0 doesn't work this.drawingContext.lineWidth = 0.0001; } else { @@ -1006,7 +1024,14 @@ class Renderer2D extends Renderer { } rotate(rad) { + if (typeof rad === 'undefined') { + const matrix = ctx.getTransform(); + console.log(matrix); + const rad = Math.atan2(/*-*/matrix.b, matrix.a); + return rad >= 0 ? rad : rad + Math.PI * 2; // angle > Math.PI } + } this.drawingContext.rotate(rad); + return this; } scale(x, y) { diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 8487dde0a5..017b452517 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -1291,6 +1291,9 @@ function loadingDisplaying(p5, fn){ */ fn.tint = function(...args) { // p5._validateParameters('tint', args); + if (args.length === 0) { + return this._renderer.states.tint; // getter + } const c = this.color(...args); this._renderer.states.setValue('tint', c._getRGBA([255, 255, 255, 255])); }; @@ -1437,6 +1440,9 @@ function loadingDisplaying(p5, fn){ */ fn.imageMode = function(m) { // p5._validateParameters('imageMode', arguments); + if (typeof m === 'undefined') { // getter + return this._renderer.states.imageMode; + } if ( m === constants.CORNER || m === constants.CORNERS || diff --git a/src/shape/attributes.js b/src/shape/attributes.js index 5669c5d32e..18188618a6 100644 --- a/src/shape/attributes.js +++ b/src/shape/attributes.js @@ -86,6 +86,9 @@ function attributes(p5, fn){ */ fn.ellipseMode = function(m) { // p5._validateParameters('ellipseMode', arguments); + if (typeof m === 'undefined') { // getter + return this._renderer?.states.ellipseMode; + } if ( m === constants.CORNER || m === constants.CORNERS || @@ -286,6 +289,9 @@ function attributes(p5, fn){ */ fn.rectMode = function(m) { // p5._validateParameters('rectMode', arguments); + if (typeof m === 'undefined') { // getter + return this._renderer?.states.ellipseMode; + } if ( m === constants.CORNER || m === constants.CORNERS || @@ -424,6 +430,9 @@ function attributes(p5, fn){ */ fn.strokeCap = function(cap) { // p5._validateParameters('strokeCap', arguments); + if (typeof cap === 'undefined') { // getter + return this._renderer.strokeCap(); + } if ( cap === constants.ROUND || cap === constants.SQUARE || @@ -523,6 +532,9 @@ function attributes(p5, fn){ */ fn.strokeJoin = function(join) { // p5._validateParameters('strokeJoin', arguments); + if (typeof join === 'undefined') { // getter + return this._renderer.strokeJoin(); + } if ( join === constants.ROUND || join === constants.BEVEL || @@ -590,8 +602,7 @@ function attributes(p5, fn){ */ fn.strokeWeight = function(w) { // p5._validateParameters('strokeWeight', arguments); - this._renderer.strokeWeight(w); - return this; + return this._renderer.strokeWeight(w); }; } From ae9af3040f05ad2f9c15545da14797a109824667 Mon Sep 17 00:00:00 2001 From: dhowe Date: Tue, 9 Dec 2025 20:13:31 +0000 Subject: [PATCH 006/250] more getters --- src/color/setting.js | 3 +-- src/core/environment.js | 4 ++++ src/core/p5.Renderer2D.js | 22 ++++++++++++++++---- src/core/transform.js | 43 ++++++++++++++++++++++++++------------- src/math/trigonometry.js | 30 ++++++++++++++++++++++++++- 5 files changed, 81 insertions(+), 21 deletions(-) diff --git a/src/color/setting.js b/src/color/setting.js index 7e1d92b995..9509f9dd3f 100644 --- a/src/color/setting.js +++ b/src/color/setting.js @@ -668,8 +668,7 @@ function setting(p5, fn){ * @chainable */ fn.background = function(...args) { - this._renderer.background(...args); - return this; + return this._renderer.background(...args); }; /** diff --git a/src/core/environment.js b/src/core/environment.js index a3226aa77b..ab4575d9bb 100644 --- a/src/core/environment.js +++ b/src/core/environment.js @@ -314,6 +314,10 @@ function environment(p5, fn, lifecycles){ fn.cursor = function(type, x, y) { let cursor = 'auto'; const canvas = this._curElement.elt; + if (typeof type === 'undefined') { + let curstr = canvas.style.cursor; + return curstr.length ? curstr : 'default'; + } if (standardCursors.includes(type)) { // Standard css cursor cursor = type; diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 635218080f..9561c91b73 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -1025,27 +1025,41 @@ class Renderer2D extends Renderer { rotate(rad) { if (typeof rad === 'undefined') { - const matrix = ctx.getTransform(); - console.log(matrix); - const rad = Math.atan2(/*-*/matrix.b, matrix.a); - return rad >= 0 ? rad : rad + Math.PI * 2; // angle > Math.PI } + const matrix = this.drawingContext.getTransform(); + let angle = this._pInst.decomposeMatrix(matrix).rotation; + if (angle < 0) { + angle += Math.PI * 2; // ensure a positive angle + } + if (this._pInst._angleMode === this._pInst.DEGREES) { + angle *= constants.RAD_TO_DEG; // to degrees + } + return Math.abs(angle); } this.drawingContext.rotate(rad); return this; } scale(x, y) { + if (typeof x === 'undefined' && typeof y === 'undefined') { + const matrix = this.drawingContext.getTransform(); + return this._pInst.decomposeMatrix(matrix).scale; + } this.drawingContext.scale(x, y); return this; } translate(x, y) { + if (typeof x === 'undefined' && typeof y === 'undefined') { + const matrix = this.drawingContext.getTransform(); + return this._pInst.decomposeMatrix(matrix).translation; + } // support passing a vector as the 1st parameter if (x instanceof p5.Vector) { y = x.y; x = x.x; } this.drawingContext.translate(x, y); + console.log('MAT:',this.drawingContext.getTransform()); return this; } diff --git a/src/core/transform.js b/src/core/transform.js index 8832d3a4ee..7db3cf8e46 100644 --- a/src/core/transform.js +++ b/src/core/transform.js @@ -462,8 +462,7 @@ function transform(p5, fn){ */ fn.rotate = function(angle, axis) { // p5._validateParameters('rotate', arguments); - this._renderer.rotate(this._toRadians(angle), axis); - return this; + return this._renderer.rotate(this._toRadians(angle), axis); }; /** @@ -598,8 +597,7 @@ function transform(p5, fn){ fn.rotateX = function(angle) { this._assert3d('rotateX'); // p5._validateParameters('rotateX', arguments); - this._renderer.rotateX(this._toRadians(angle)); - return this; + return this._renderer.rotateX(this._toRadians(angle)); }; /** @@ -734,8 +732,7 @@ function transform(p5, fn){ fn.rotateY = function(angle) { this._assert3d('rotateY'); // p5._validateParameters('rotateY', arguments); - this._renderer.rotateY(this._toRadians(angle)); - return this; + return this._renderer.rotateY(this._toRadians(angle)); }; /** @@ -870,8 +867,7 @@ function transform(p5, fn){ fn.rotateZ = function(angle) { this._assert3d('rotateZ'); // p5._validateParameters('rotateZ', arguments); - this._renderer.rotateZ(this._toRadians(angle)); - return this; + return this._renderer.rotateZ(this._toRadians(angle)); }; /** @@ -1060,9 +1056,7 @@ function transform(p5, fn){ z = 1; } - this._renderer.scale(x, y, z); - - return this; + return this._renderer.scale(x, y, z); }; /** @@ -1137,6 +1131,17 @@ function transform(p5, fn){ */ fn.shearX = function(angle) { // p5._validateParameters('shearX', arguments); + if (typeof angle === 'undefined') { + let matrix = this._renderer.drawingContext.getTransform(); + let rad = this.decomposeMatrix(matrix).skew.x; + if (rad < 0) { + rad += Math.PI * 2; // ensure a positive angle + } + if (fn._angleMode === fn.DEGREES) { + rad *= fn.RAD_TO_DEG; // to degrees + } + return rad; + } const rad = this._toRadians(angle); this._renderer.applyMatrix(1, 0, Math.tan(rad), 1, 0, 0); return this; @@ -1214,6 +1219,17 @@ function transform(p5, fn){ */ fn.shearY = function(angle) { // p5._validateParameters('shearY', arguments); + if (typeof angle === 'undefined') { + let matrix = this._renderer.drawingContext.getTransform(); + let rad = this.decomposeMatrix(matrix).skew.y; + if (rad < 0) { + rad += Math.PI * 2; // ensure a positive angle + } + if (fn._angleMode === fn.DEGREES) { + rad *= fn.RAD_TO_DEG; // to degrees + } + return rad; + } const rad = this._toRadians(angle); this._renderer.applyMatrix(1, Math.tan(rad), 0, 1, 0, 0); return this; @@ -1398,11 +1414,10 @@ function transform(p5, fn){ fn.translate = function(x, y, z) { // p5._validateParameters('translate', arguments); if (this._renderer.isP3D) { - this._renderer.translate(x, y, z); + return this._renderer.translate(x, y, z); } else { - this._renderer.translate(x, y); + return this._renderer.translate(x, y); } - return this; }; /** diff --git a/src/math/trigonometry.js b/src/math/trigonometry.js index 608ac60743..e3e2fa12bc 100644 --- a/src/math/trigonometry.js +++ b/src/math/trigonometry.js @@ -226,6 +226,33 @@ function trigonometry(p5, fn){ return this._fromRadians(Math.asin(ratio)); }; + fn.decomposeMatrix = function(mat) { + // adapted from https://frederic-wang.fr/2013/12/01/decomposition-of-2d-transform-matrices/ + let { a, b, c, d, e, f } = mat; + let delta = a * d - b * c; + let result = { + translation: { x: e, y: f }, + scale: { x: 0, y: 0 }, + skew: { x: 0, y: 0 }, + rotation: 0 + }; + if (a !== 0 || b !== 0) { + let r = Math.sqrt(a * a + b * b); + result.rotation = b > 0 ? Math.acos(a / r) : -Math.acos(a / r); + result.scale = { x: r, y: delta / r }; + result.skew = { x: Math.atan((a * c + b * d) / (r * r)), y: 0 }; + } else if (c !== 0 || d !== 0) { + let s = Math.sqrt(c * c + d * d); + result.rotation = Math.PI / 2 - + (d > 0 ? Math.acos(-c / s) : -Math.acos(c / s)); + result.scale = { x: delta / s, y: s }; + result.skew = { x: 0, y: Math.atan((a * c + b * d) / (s * s)) }; + } else { + // a = b = c = d = 0 + } + return result; + }; + /** * Calculates the arc tangent of a number. * @@ -864,7 +891,8 @@ function trigonometry(p5, fn){ * @returns {Number} */ fn._toRadians = function(angle) { - if (this._angleMode === DEGREES) { + // returns undefined if no argument + if (typeof angle !== 'undefined' && this._angleMode === DEGREES) { return angle * constants.DEG_TO_RAD; } return angle; From 8f7a431ff4f18a1623cf99d1fe600fa2872d77dd Mon Sep 17 00:00:00 2001 From: dhowe Date: Wed, 10 Dec 2025 17:38:48 +0000 Subject: [PATCH 007/250] complete first set of getters --- src/core/p5.Renderer.js | 15 +++++++-------- src/core/p5.Renderer2D.js | 23 +++++++++++++++-------- src/core/transform.js | 10 ++-------- src/image/loading_displaying.js | 2 +- src/math/trigonometry.js | 6 +++--- src/type/textCore.js | 7 +++++++ 6 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index f2d60f4dac..87c5241e75 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -35,6 +35,7 @@ class ClonableObject { class Renderer { static states = { + background: null, strokeColor: null, strokeSet: false, fillColor: null, @@ -45,6 +46,11 @@ class Renderer { rectMode: constants.CORNER, ellipseMode: constants.CENTER, strokeWeight: 1, + bezierOrder: 3, + splineProperties: new ClonableObject({ + ends: constants.INCLUDE, + tightness: 0 + }), textFont: { family: 'sans-serif' }, textLeading: 15, @@ -52,15 +58,8 @@ class Renderer { textSize: 12, textAlign: constants.LEFT, textBaseline: constants.BASELINE, - bezierOrder: 3, - splineProperties: new ClonableObject({ - ends: constants.INCLUDE, - tightness: 0 - }), textWrap: constants.WORD, - - // added v2.0 - fontStyle: constants.NORMAL, // v1: textStyle + fontStyle: constants.NORMAL, // v1: was textStyle fontStretch: constants.NORMAL, fontWeight: constants.NORMAL, lineHeight: constants.NORMAL, diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 9561c91b73..ad92ec5e71 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -167,16 +167,18 @@ class Renderer2D extends Renderer { background(...args) { this.push(); this.resetMatrix(); - + if (args.length === 0) { + // getter (#8278) + return this.states.background; + } if (args[0] instanceof Image) { + const img = args[0]; 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); } + this._pInst.image(img, 0, 0, this.width, this.height); + this.states.background = img; // save for getter (#8278) } else { // create background rect const color = this._pInst.color(...args); @@ -199,6 +201,7 @@ class Renderer2D extends Renderer { if (this._isErasing) { this._pInst.erase(); } + this.states.background = color; // save for getter (#8278) } this.pop(); } @@ -1044,6 +1047,11 @@ class Renderer2D extends Renderer { const matrix = this.drawingContext.getTransform(); return this._pInst.decomposeMatrix(matrix).scale; } + // support passing objects with x,y properties (including p5.Vector) + if (typeof x === 'object' && 'x' in x && 'y' in x) { + y = x.y; + x = x.x; + } this.drawingContext.scale(x, y); return this; } @@ -1053,13 +1061,12 @@ class Renderer2D extends Renderer { const matrix = this.drawingContext.getTransform(); return this._pInst.decomposeMatrix(matrix).translation; } - // support passing a vector as the 1st parameter - if (x instanceof p5.Vector) { + // support passing objects with x,y properties (including p5.Vector) + if (typeof x === 'object' && 'x' in x && 'y' in x) { y = x.y; x = x.x; } this.drawingContext.translate(x, y); - console.log('MAT:',this.drawingContext.getTransform()); return this; } diff --git a/src/core/transform.js b/src/core/transform.js index 7db3cf8e46..133a9da9cc 100644 --- a/src/core/transform.js +++ b/src/core/transform.js @@ -1133,10 +1133,7 @@ function transform(p5, fn){ // p5._validateParameters('shearX', arguments); if (typeof angle === 'undefined') { let matrix = this._renderer.drawingContext.getTransform(); - let rad = this.decomposeMatrix(matrix).skew.x; - if (rad < 0) { - rad += Math.PI * 2; // ensure a positive angle - } + let rad = this.decomposeMatrix(matrix).shear.x; if (fn._angleMode === fn.DEGREES) { rad *= fn.RAD_TO_DEG; // to degrees } @@ -1221,10 +1218,7 @@ function transform(p5, fn){ // p5._validateParameters('shearY', arguments); if (typeof angle === 'undefined') { let matrix = this._renderer.drawingContext.getTransform(); - let rad = this.decomposeMatrix(matrix).skew.y; - if (rad < 0) { - rad += Math.PI * 2; // ensure a positive angle - } + let rad = this.decomposeMatrix(matrix).shear.y; if (fn._angleMode === fn.DEGREES) { rad *= fn.RAD_TO_DEG; // to degrees } diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 017b452517..9617b2ce3c 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -1292,7 +1292,7 @@ function loadingDisplaying(p5, fn){ fn.tint = function(...args) { // p5._validateParameters('tint', args); if (args.length === 0) { - return this._renderer.states.tint; // getter + return this.color(this._renderer.states.tint); // getter } const c = this.color(...args); this._renderer.states.setValue('tint', c._getRGBA([255, 255, 255, 255])); diff --git a/src/math/trigonometry.js b/src/math/trigonometry.js index e3e2fa12bc..28fe4fcf3a 100644 --- a/src/math/trigonometry.js +++ b/src/math/trigonometry.js @@ -233,20 +233,20 @@ function trigonometry(p5, fn){ let result = { translation: { x: e, y: f }, scale: { x: 0, y: 0 }, - skew: { x: 0, y: 0 }, + shear: { x: 0, y: 0 }, rotation: 0 }; if (a !== 0 || b !== 0) { let r = Math.sqrt(a * a + b * b); result.rotation = b > 0 ? Math.acos(a / r) : -Math.acos(a / r); result.scale = { x: r, y: delta / r }; - result.skew = { x: Math.atan((a * c + b * d) / (r * r)), y: 0 }; + result.shear = { x: Math.atan((a * c + b * d) / (r * r)), y: 0 }; } else if (c !== 0 || d !== 0) { let s = Math.sqrt(c * c + d * d); result.rotation = Math.PI / 2 - (d > 0 ? Math.acos(-c / s) : -Math.acos(c / s)); result.scale = { x: delta / s, y: s }; - result.skew = { x: 0, y: Math.atan((a * c + b * d) / (s * s)) }; + result.shear = { x: 0, y: Math.atan((a * c + b * d) / (s * s)) }; } else { // a = b = c = d = 0 } diff --git a/src/type/textCore.js b/src/type/textCore.js index 70c05cf927..4f6e555601 100644 --- a/src/type/textCore.js +++ b/src/type/textCore.js @@ -1611,7 +1611,14 @@ function textCore(p5, fn) { // the setter if (typeof h !== 'undefined') { + // accept what is returned from the getter + if (h.hasOwnProperty('horizontal')) { + h = h.horizontal; + } this.states.setValue('textAlign', h); + if (h.hasOwnProperty('vertical') && typeof v === 'undefined') { + v = h.vertical; + } if (typeof v !== 'undefined') { if (v === fn.CENTER) { v = textCoreConstants._CTX_MIDDLE; From 039ef43bc08ddf32981092fc93c88508ba6c6c8f Mon Sep 17 00:00:00 2001 From: dhowe Date: Wed, 10 Dec 2025 21:58:29 +0000 Subject: [PATCH 008/250] update parameter json, add tests for getters --- docs/parameterData.json | 52 +++++++++++++++++++-------- src/core/p5.Renderer2D.js | 14 ++++---- src/shape/attributes.js | 2 +- test/unit/core/properties.js | 69 ++++++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 22 deletions(-) create mode 100644 test/unit/core/properties.js diff --git a/docs/parameterData.json b/docs/parameterData.json index ab5f651595..543787aa80 100644 --- a/docs/parameterData.json +++ b/docs/parameterData.json @@ -162,6 +162,7 @@ }, "background": { "overloads": [ + [], [ "p5.Color" ], @@ -201,6 +202,7 @@ }, "colorMode": { "overloads": [ + [], [ "RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH", "Number?" @@ -217,6 +219,7 @@ }, "fill": { "overloads": [ + [], [ "Number", "Number", @@ -250,6 +253,7 @@ }, "stroke": { "overloads": [ + [], [ "Number", "Number", @@ -286,6 +290,7 @@ }, "blendMode": { "overloads": [ + [], [ "BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|REMOVE|SUBTRACT" ] @@ -303,6 +308,7 @@ }, "cursor": { "overloads": [ + [], [ "ARROW|CROSS|HAND|MOVE|TEXT|WAIT|String", "Number?", @@ -528,28 +534,32 @@ [ "Number", "p5.Vector|Number[]?" - ] + ], + [] ] }, "rotateX": { "overloads": [ [ "Number" - ] + ], + [] ] }, "rotateY": { "overloads": [ [ "Number" - ] + ], + [] ] }, "rotateZ": { "overloads": [ [ "Number" - ] + ], + [] ] }, "scale": { @@ -561,21 +571,24 @@ ], [ "p5.Vector|Number[]" - ] + ], + [] ] }, "shearX": { "overloads": [ [ "Number" - ] + ], + [] ] }, "shearY": { "overloads": [ [ "Number" - ] + ], + [] ] }, "translate": { @@ -587,7 +600,8 @@ ], [ "p5.Vector" - ] + ], + [] ] }, "push": { @@ -1014,7 +1028,8 @@ ], [ "p5.Color" - ] + ], + [] ] }, "noTint": { @@ -1026,7 +1041,8 @@ "overloads": [ [ "CORNER|CORNERS|CENTER" - ] + ], + [] ] }, "blend": { @@ -1748,7 +1764,8 @@ "overloads": [ [ "CENTER|RADIUS|CORNER|CORNERS" - ] + ], + [] ] }, "noSmooth": { @@ -1758,6 +1775,7 @@ }, "rectMode": { "overloads": [ + [], [ "CENTER|RADIUS|CORNER|CORNERS" ] @@ -1772,21 +1790,24 @@ "overloads": [ [ "ROUND|SQUARE|PROJECT" - ] + ], + [] ] }, "strokeJoin": { "overloads": [ [ "MITER|BEVEL|ROUND" - ] + ], + [] ] }, "strokeWeight": { "overloads": [ [ "Number" - ] + ], + [] ] }, "bezier": { @@ -2108,7 +2129,8 @@ [ "LEFT|CENTER|RIGHT?", "TOP|BOTTOM|CENTER|BASELINE?" - ] + ], + [] ] }, "textAscent": { diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index ad92ec5e71..e75a108a7a 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -165,12 +165,12 @@ class Renderer2D extends Renderer { ////////////////////////////////////////////// background(...args) { - this.push(); - this.resetMatrix(); if (args.length === 0) { - // getter (#8278) - return this.states.background; + return this.states.background; // getter (#8278) } + let bgForState = null; + this.push(); + this.resetMatrix(); if (args[0] instanceof Image) { const img = args[0]; if (args[1] >= 0) { @@ -178,7 +178,7 @@ class Renderer2D extends Renderer { this.drawingContext.globalAlpha = args[1] / 255; } this._pInst.image(img, 0, 0, this.width, this.height); - this.states.background = img; // save for getter (#8278) + bgForState = img; // save for getter (#8278) } else { // create background rect const color = this._pInst.color(...args); @@ -201,9 +201,11 @@ class Renderer2D extends Renderer { if (this._isErasing) { this._pInst.erase(); } - this.states.background = color; // save for getter (#8278) + bgForState = color; // save for getter (#8278) } this.pop(); + + this.states.setValue('background', bgForState); // set state (#8278) } clear() { diff --git a/src/shape/attributes.js b/src/shape/attributes.js index 18188618a6..7ac2a8618e 100644 --- a/src/shape/attributes.js +++ b/src/shape/attributes.js @@ -290,7 +290,7 @@ function attributes(p5, fn){ fn.rectMode = function(m) { // p5._validateParameters('rectMode', arguments); if (typeof m === 'undefined') { // getter - return this._renderer?.states.ellipseMode; + return this._renderer?.states.rectMode; } if ( m === constants.CORNER || diff --git a/test/unit/core/properties.js b/test/unit/core/properties.js new file mode 100644 index 0000000000..8c58a73e2f --- /dev/null +++ b/test/unit/core/properties.js @@ -0,0 +1,69 @@ +import p5 from '../../../src/app.js'; + +suite('Set/get properties', function() { + + let p = new p5(function (sketch) { + sketch.setup = function () { }; + sketch.draw = function () { }; + }); + + /*beforeEach(function () { + myp5 = new p5(function (p) { + p.setup = function () { }; + p.draw = function () { }; + }); + }); + afterEach(function () { + myp5.remove(); + });*/ + + let getters = { + background: new p5.Color([100, 100, 50]), + fill: new p5.Color([100, 200, 50]), + stroke: new p5.Color([200, 100, 50, 100]), + tint: new p5.Color([100, 140, 50]), + + rectMode: p.CENTER, + colorMode: p.HSB, + blendMode: 'source-over', + imageMode: p.CORNER, + ellipseMode: p.CORNER, + + strokeWeight: 6, + strokeCap: p.ROUND, + strokeJoin: p.MITER, + pixelDensity: 1, + cursor: 'pointer', + + rotate: p.PI, + translate: { x: 1, y: 2 }, + scale: { x: 1, y: 2 }, + + textAlign: { horizontal: p.CENTER, vertical: p.CENTER }, + textLeading: 18, + textFont: 'arial', + textSize: 1, + textStyle: 1, + textWrap: p.WORD, + textDirection: 1, + textWeight: 1 + }; + + Object.keys(getters).forEach(prop => { + let arg = getters[prop]; + test(`${prop}()`, function() { + // setter + if (typeof arg === 'object' && !(arg instanceof p5.Color)) { + p[prop](...Object.values(arg)); // set with object + } + else if (Array.isArray(arg)) { + p[prop](...arg); // set with array + } + else { + p[prop](arg); // set with primitive + } + // getter + assert.strictEqual(p[prop]().toString(), arg.toString(), `${arg.toString()}`); + }); + }); +}); From ece3986450895c152612193a835485ce903ce22b Mon Sep 17 00:00:00 2001 From: dhowe Date: Wed, 10 Dec 2025 22:13:39 +0000 Subject: [PATCH 009/250] add bezierOrder and splineProperties to tests --- test/unit/core/properties.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/unit/core/properties.js b/test/unit/core/properties.js index 8c58a73e2f..10bcce9162 100644 --- a/test/unit/core/properties.js +++ b/test/unit/core/properties.js @@ -38,7 +38,8 @@ suite('Set/get properties', function() { rotate: p.PI, translate: { x: 1, y: 2 }, scale: { x: 1, y: 2 }, - + bezierOrder: 2, + splineProperties: { ends: p.EXCLUDE, tightness: -5 }, textAlign: { horizontal: p.CENTER, vertical: p.CENTER }, textLeading: 18, textFont: 'arial', @@ -52,6 +53,7 @@ suite('Set/get properties', function() { Object.keys(getters).forEach(prop => { let arg = getters[prop]; test(`${prop}()`, function() { + // setter if (typeof arg === 'object' && !(arg instanceof p5.Color)) { p[prop](...Object.values(arg)); // set with object From d31f77c2316653db517ac7d363ee541b957e96a9 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Tue, 23 Dec 2025 12:42:32 +0000 Subject: [PATCH 010/250] Update dependency and package gifenc --- package-lock.json | 8 ++++---- package.json | 2 +- rollup.config.mjs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6be97b491a..7fb7e54354 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.2.0-rc.3", "license": "LGPL-2.1", "dependencies": { - "@davepagurek/bezier-path": "^0.0.2", + "@davepagurek/bezier-path": "^0.0.7", "@japont/unicode-range": "^1.0.0", "acorn": "^8.12.1", "acorn-walk": "^8.3.4", @@ -355,9 +355,9 @@ } }, "node_modules/@davepagurek/bezier-path": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@davepagurek/bezier-path/-/bezier-path-0.0.2.tgz", - "integrity": "sha512-4L9ddgzZc9DRGyl1RrS3z5nwnVJoyjsAelVG4X1jh4tVxryEHr4H9QavhxW/my6Rn3669Qz6mhv8gd5O/WeFTA==", + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@davepagurek/bezier-path/-/bezier-path-0.0.7.tgz", + "integrity": "sha512-CVlnCOrV1iy4Z12T756i9l4G6kF7r8uhlnb+xqDemAMmWQB+8Q0b+8VEqIiUfywgZDSiDr18Rm7pZlnA69rE8Q==", "license": "MIT" }, "node_modules/@es-joy/jsdoccomment": { diff --git a/package.json b/package.json index a631f6ba88..21d6b92aad 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "version": "2.2.0-rc.3", "dependencies": { - "@davepagurek/bezier-path": "^0.0.2", + "@davepagurek/bezier-path": "^0.0.7", "@japont/unicode-range": "^1.0.0", "acorn": "^8.12.1", "acorn-walk": "^8.3.4", diff --git a/rollup.config.mjs b/rollup.config.mjs index ce03d12124..208499561c 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -193,7 +193,7 @@ export default [ format: 'es', dir: 'dist' }, - external: /node_modules/, + external: /node_modules\/(?!gifenc)/, plugins }, ...generateModuleBuild() From f645136834506b3584e7e9b41800aef5e44a62b5 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 24 Dec 2025 16:08:29 +0000 Subject: [PATCH 011/250] Move some references from window to globalThis --- package.json | 1 + src/core/environment.js | 5 ++--- src/core/main.js | 10 +++++----- src/shape/2d_primitives.js | 1 - 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 21d6b92aad..0815655f85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "p5", "repository": "processing/p5.js", + "type": "module", "scripts": { "build": "rollup -c", "dev": "vite preview/", diff --git a/src/core/environment.js b/src/core/environment.js index 88867ef78f..480d1de1fc 100644 --- a/src/core/environment.js +++ b/src/core/environment.js @@ -13,7 +13,7 @@ function environment(p5, fn, lifecycles){ const standardCursors = [C.ARROW, C.CROSS, C.HAND, C.MOVE, C.TEXT, C.WAIT]; fn._frameRate = 0; - fn._lastFrameTime = window.performance.now(); + fn._lastFrameTime = globalThis.performance.now(); fn._targetFrameRate = 60; const _windowPrint = window.print; @@ -868,7 +868,6 @@ function environment(p5, fn, lifecycles){ * */ fn.fullscreen = function(val) { - // p5._validateParameters('fullscreen', arguments); // no arguments, return fullscreen or not if (typeof val === 'undefined') { return ( @@ -946,7 +945,6 @@ function environment(p5, fn, lifecycles){ * @returns {Number} current pixel density of the sketch. */ fn.pixelDensity = function(val) { - // p5._validateParameters('pixelDensity', arguments); let returnValue; if (typeof val === 'number') { if (val !== this._renderer._pixelDensity) { @@ -1271,6 +1269,7 @@ function environment(p5, fn, lifecycles){ const screenPosition = matrix.multiplyAndNormalizePoint(worldPosition); return screenPosition; }; + /** * Converts 2D screen coordinates to 3D world coordinates. * diff --git a/src/core/main.js b/src/core/main.js index 8e2d292f1d..c522fbf90a 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -245,7 +245,7 @@ class p5 { // 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(); + this._millisStart = globalThis.performance.now(); const context = this._isGlobal ? window : this; if (typeof context.setup === 'function') { @@ -266,8 +266,8 @@ class p5 { } } - this._lastTargetFrameTime = window.performance.now(); - this._lastRealFrameTime = window.performance.now(); + this._lastTargetFrameTime = globalThis.performance.now(); + this._lastRealFrameTime = globalThis.performance.now(); this._setupDone = true; if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { this._updateAccsOutput(); @@ -278,7 +278,7 @@ class p5 { // Record the time when the draw loop starts so that millis() starts at 0 // when the draw loop begins. - this._millisStart = window.performance.now(); + this._millisStart = globalThis.performance.now(); } // While '#_draw' here is async, it is not awaited as 'requestAnimationFrame' @@ -288,7 +288,7 @@ class p5 { // and 'postdraw'. async _draw(requestAnimationFrameTimestamp) { if (this.hitCriticalError) return; - const now = requestAnimationFrameTimestamp || window.performance.now(); + const now = requestAnimationFrameTimestamp || globalThis.performance.now(); const timeSinceLastFrame = now - this._lastTargetFrameTime; const targetTimeBetweenFrames = 1000 / this._targetFrameRate; diff --git a/src/shape/2d_primitives.js b/src/shape/2d_primitives.js index 96a5d530e8..8569082bb9 100644 --- a/src/shape/2d_primitives.js +++ b/src/shape/2d_primitives.js @@ -1214,7 +1214,6 @@ function primitives(p5, fn){ * * */ - /** * @method rect * @param {Number} x From ba3407d2ba36f32bef7a2545f92815421511e1cf Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 24 Dec 2025 18:02:09 +0000 Subject: [PATCH 012/250] Have core work in node.js Uses in node.js will need additional reimplementation of functionalities otherwise provided by window or document. --- src/core/environment.js | 39 ++++++++++------------- src/core/main.js | 70 +++++++++++++++++++++++------------------ src/core/p5.Renderer.js | 4 ++- 3 files changed, 59 insertions(+), 54 deletions(-) diff --git a/src/core/environment.js b/src/core/environment.js index 480d1de1fc..7175319c14 100644 --- a/src/core/environment.js +++ b/src/core/environment.js @@ -11,12 +11,13 @@ import * as C from './constants'; function environment(p5, fn, lifecycles){ const standardCursors = [C.ARROW, C.CROSS, C.HAND, C.MOVE, C.TEXT, C.WAIT]; + const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'; fn._frameRate = 0; fn._lastFrameTime = globalThis.performance.now(); fn._targetFrameRate = 60; - const _windowPrint = window.print; + const windowPrint = isBrowser ? window.print : null; let windowPrintDisabled = false; lifecycles.presetup = function(){ @@ -24,11 +25,13 @@ function environment(p5, fn, lifecycles){ 'resize' ]; - for(const event of events){ - window.addEventListener(event, this[`_on${event}`].bind(this), { - passive: false, - signal: this._removeSignal - }); + if(isBrowser){ + for(const event of events){ + window.addEventListener(event, this[`_on${event}`].bind(this), { + passive: false, + signal: this._removeSignal + }); + } } }; @@ -64,9 +67,9 @@ function environment(p5, fn, lifecycles){ * */ fn.print = function(...args) { - if (!args.length) { + if (!args.length && windowPrint !== null) { if (!windowPrintDisabled) { - _windowPrint(); + windowPrint(); if ( window.confirm( 'You just tried to print the webpage. Do you want to prevent this from running again?' @@ -218,7 +221,7 @@ function environment(p5, fn, lifecycles){ * * */ - fn.focused = document.hasFocus(); + fn.focused = isBrowser ? document.hasFocus() : true; /** * Changes the cursor's appearance. @@ -628,7 +631,7 @@ function environment(p5, fn, lifecycles){ * @alt * This example does not render anything. */ - fn.displayWidth = screen.width; + fn.displayWidth = isBrowser ? window.screen.width : 0; /** * A `Number` variable that stores the height of the screen display. @@ -659,7 +662,7 @@ function environment(p5, fn, lifecycles){ * @alt * This example does not render anything. */ - fn.displayHeight = screen.height; + fn.displayHeight = isBrowser ? window.screen.height : 0; /** * A `Number` variable that stores the width of the browser's viewport. @@ -794,21 +797,11 @@ function environment(p5, fn, lifecycles){ }; function getWindowWidth() { - return ( - window.innerWidth || - (document.documentElement && document.documentElement.clientWidth) || - (document.body && document.body.clientWidth) || - 0 - ); + return isBrowser ? document.documentElement.clientWidth : 0; } function getWindowHeight() { - return ( - window.innerHeight || - (document.documentElement && document.documentElement.clientHeight) || - (document.body && document.body.clientHeight) || - 0 - ); + return isBrowser ? document.documentElement.clientHeight : 0; } /** diff --git a/src/core/main.js b/src/core/main.js index c522fbf90a..29dc532cf9 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -148,19 +148,24 @@ class p5 { 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') { + + if(typeof window !== 'undefined'){ + 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); + } + }else{ this.#_start(); - } else { - this._startListener = this.#_start.bind(this); - window.addEventListener('load', this._startListener, false); } } @@ -237,11 +242,13 @@ class p5 { // Always create a default canvas. // Later on if the user calls createCanvas, this default one // will be replaced - this.createCanvas( - 100, - 100, - constants.P2D - ); + if(typeof window !== 'undefined'){ + this.createCanvas( + 100, + 100, + constants.P2D + ); + } // Record the time when setup starts. millis() will start at 0 within // setup, but this isn't documented, locked-in behavior yet. @@ -253,16 +260,18 @@ class p5 { } 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; + if(typeof document !== 'undefined'){ + 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; + } } } @@ -330,9 +339,10 @@ class p5 { // get notified the next time the browser gives us // an opportunity to draw. if (this._loop) { - this._requestAnimId = window.requestAnimationFrame( - this._draw.bind(this) - ); + const boundDraw = this._draw.bind(this); + this._requestAnimId = typeof window !== 'undefined' ? + window.requestAnimationFrame(boundDraw) : + setImmediate(boundDraw); } } diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 04caa1144d..1d5ebcc64d 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -72,7 +72,9 @@ class Renderer { this._pInst = pInst; this._isMainCanvas = isMainCanvas; this.pixels = []; - this._pixelDensity = Math.ceil(window.devicePixelRatio) || 1; + this._pixelDensity = typeof window !== 'undefined' ? + Math.ceil(window.devicePixelRatio) : + 1; this.width = w; this.height = h; From c4700f1536cce0f48ebac52d60614c4d95225472 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 24 Dec 2025 22:31:04 +0000 Subject: [PATCH 013/250] Guard against a few references to window --- src/core/init.js | 26 +++++++++++++++----------- src/type/lib/Typr.js | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/core/init.js b/src/core/init.js index 764437b1ff..0ba781e406 100644 --- a/src/core/init.js +++ b/src/core/init.js @@ -13,6 +13,7 @@ import { initialize as initTranslator } from './internationalization'; * @return {Undefined} */ export const _globalInit = () => { + if(typeof window === 'undefined') return; // Could have been any property defined within the p5 constructor. // If that property is already a part of the global object, // this code has already run before, likely due to a duplicate import @@ -40,17 +41,20 @@ export const _globalInit = () => { }; // make a promise that resolves when the document is ready -export const waitForDocumentReady = () => - new Promise((resolve, reject) => { - // if the page is ready, initialize p5 immediately - if (document.readyState === 'complete') { - resolve(); - // if the page is still loading, add an event listener - // and initialize p5 as soon as it finishes loading - } else { - window.addEventListener('load', resolve, false); - } - }); +export const waitForDocumentReady = () =>{ + if(typeof document !== 'undefined'){ + return new Promise((resolve, reject) => { + // if the page is ready, initialize p5 immediately + if (document.readyState === 'complete') { + resolve(); + // if the page is still loading, add an event listener + // and initialize p5 as soon as it finishes loading + } else { + window.addEventListener('load', resolve, false); + } + }); + } +}; // only load translations if we're using the full, un-minified library export const waitingForTranslator = diff --git a/src/type/lib/Typr.js b/src/type/lib/Typr.js index e81fcb58f1..ec7e94a5ec 100644 --- a/src/type/lib/Typr.js +++ b/src/type/lib/Typr.js @@ -323,7 +323,7 @@ Typr["B"] = { } return s; }, - _tdec: window["TextDecoder"] ? new window["TextDecoder"]() : null, + _tdec: globalThis["TextDecoder"] ? new globalThis["TextDecoder"]() : null, readUTF8: function (buff, p, l) { var tdec = Typr["B"]._tdec; if (tdec && p == 0 && l == buff.length) return tdec["decode"](buff); From 130d67f29d0dc0dec9dbcf631bb9f90aecd3414e Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Thu, 25 Dec 2025 15:13:04 +0000 Subject: [PATCH 014/250] Use ES6 import in visual report script --- visual-report.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/visual-report.js b/visual-report.js index 12e21b6261..bffc39dacb 100644 --- a/visual-report.js +++ b/visual-report.js @@ -1,5 +1,5 @@ -const fs = require('fs'); -const path = require('path'); +import fs from 'fs'; +import path from 'path'; const SLASH_REGEX = /\//g; async function generateVisualReport() { @@ -425,4 +425,4 @@ if (require.main === module) { }); } -module.exports = { generateVisualReport }; \ No newline at end of file +export { generateVisualReport }; From b1dcca7a95de1cdbd9eea6751d62d23d28a7378b Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Thu, 25 Dec 2025 15:28:09 +0000 Subject: [PATCH 015/250] Update CI node version --- .github/workflows/ci-lint.yml | 4 ++-- .github/workflows/ci-test.yml | 4 ++-- .github/workflows/release-workflow-v2.yml | 2 +- .github/workflows/release-workflow.yml | 2 +- visual-report.js | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-lint.yml b/.github/workflows/ci-lint.yml index 9410a25a43..ea1eccbee2 100644 --- a/.github/workflows/ci-lint.yml +++ b/.github/workflows/ci-lint.yml @@ -14,10 +14,10 @@ jobs: steps: - uses: actions/checkout@v1 - - name: Use Node.js 20.x + - name: Use Node.js 22.x uses: actions/setup-node@v1 with: - node-version: 20.x + node-version: 22.x - name: Get node modules run: npm ci env: diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index fda1bb81d1..e5ceb912c1 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -24,10 +24,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use Node.js 20.x + - name: Use Node.js 22.x uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x - name: Verify Chrome (Ubuntu) if: matrix.os == 'ubuntu-latest' && matrix.browser == 'chrome' diff --git a/.github/workflows/release-workflow-v2.yml b/.github/workflows/release-workflow-v2.yml index 907c64596a..6574cc0e88 100644 --- a/.github/workflows/release-workflow-v2.yml +++ b/.github/workflows/release-workflow-v2.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 20 + node-version: 22 - name: Get semver info id: semver uses: akshens/semver-tag@v4 diff --git a/.github/workflows/release-workflow.yml b/.github/workflows/release-workflow.yml index 6c68dd09ff..714f0890d0 100644 --- a/.github/workflows/release-workflow.yml +++ b/.github/workflows/release-workflow.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 20 + node-version: 22 - name: Get semver info id: semver uses: akshens/semver-tag@v4 diff --git a/visual-report.js b/visual-report.js index bffc39dacb..3a29e223cb 100644 --- a/visual-report.js +++ b/visual-report.js @@ -418,7 +418,7 @@ async function generateVisualReport() { } // Run the function if this script is executed directly -if (require.main === module) { +if (import.meta.main === true) { generateVisualReport().catch(error => { console.error('Failed to generate report:', error); process.exit(1); From b417ba994bcb9b176bb0d8854bd632c993c03fcf Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Thu, 25 Dec 2025 15:39:04 +0000 Subject: [PATCH 016/250] Provide a node compatible export --- package.json | 4 ++++ src/app.node.js | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/app.node.js diff --git a/package.json b/package.json index 0815655f85..1e66029484 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,10 @@ "types": "./types/global.d.ts", "default": "./dist/app.js" }, + "./node": { + "types": "./types/p5.d.ts", + "default": "./dist/app.node.js" + }, "./core": { "default": "./dist/core/main.js" }, diff --git a/src/app.node.js b/src/app.node.js new file mode 100644 index 0000000000..ef270459b7 --- /dev/null +++ b/src/app.node.js @@ -0,0 +1,60 @@ +// core +import p5 from './core/main'; + +// shape +import shape from './shape'; +shape(p5); + +//accessibility +import accessibility from './accessibility'; +accessibility(p5); + +// color +import color from './color'; +color(p5); + +// core +// currently, it only contains the test for parameter validation +// import friendlyErrors from './core/friendly_errors'; +// friendlyErrors(p5); + +// data +import data from './data'; +data(p5); + +// DOM +import dom from './dom'; +dom(p5); + +// image +import image from './image'; +image(p5); + +// io +import io from './io'; +io(p5); + +// math +import math from './math'; +math(p5); + +// utilities +import utilities from './utilities'; +utilities(p5); + +// webgl +import webgl from './webgl'; +webgl(p5); + +// typography +import type from './type'; +type(p5); + +// Shaders + filters +import shader from './webgl/p5.Shader'; +p5.registerAddon(shader); +import strands from './strands/p5.strands'; +p5.registerAddon(strands); + +export default p5; + From 1b59ab3db2d1ff63beafdb60d71a97cf42f90685 Mon Sep 17 00:00:00 2001 From: Reshma R Date: Tue, 11 Nov 2025 14:11:51 +0900 Subject: [PATCH 017/250] fixes bug in #8215 --- src/math/p5.Vector.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 3f4adde462..9dccb25a68 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -2189,7 +2189,21 @@ class Vector { */ setHeading(a) { if (this.isPInst) a = this._toRadians(a); - let m = this.mag(); + if (this.z !== 0) { + p5._friendlyError( + 'p5.Vector.setHeading() only supports 2D vectors (z === 0). ' + + 'For 3D or higher-dimensional vectors, use rotate() or another ' + + 'appropriate method instead.', + 'p5.Vector.setHeading' + ); + return this; + } + const m = this.mag(); + if (m === 0) { + this.x = 0; + this.y = 0; + return this; + } this.x = m * Math.cos(a); this.y = m * Math.sin(a); return this; From bd3cb736234f698c46f24ffc0ee0a4ff84eebe59 Mon Sep 17 00:00:00 2001 From: Reshma R Date: Wed, 31 Dec 2025 19:12:28 +0900 Subject: [PATCH 018/250] fix --- src/math/p5.Vector.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 9dccb25a68..2ba6305211 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -2189,21 +2189,19 @@ class Vector { */ setHeading(a) { if (this.isPInst) a = this._toRadians(a); - if (this.z !== 0) { - p5._friendlyError( - 'p5.Vector.setHeading() only supports 2D vectors (z === 0). ' + - 'For 3D or higher-dimensional vectors, use rotate() or another ' + - 'appropriate method instead.', - 'p5.Vector.setHeading' - ); + if (this.y === undefined || (this.z !== undefined && this.z !== 0)) { + const p5inst = this.isPInst ? this._pInst : (typeof p5 !== 'undefined' ? p5 : null); + if (p5inst && p5inst._friendlyError) { + p5inst._friendlyError( + 'p5.Vector.setHeading() only supports 2D vectors (z === 0). ' + + 'For 3D or higher-dimensional vectors, use rotate() or another ' + + 'appropriate method instead.', + 'p5.Vector.setHeading' + ); + } return this; } const m = this.mag(); - if (m === 0) { - this.x = 0; - this.y = 0; - return this; - } this.x = m * Math.cos(a); this.y = m * Math.sin(a); return this; From dd492901d19f70aec69ad9c6a86b94f51434de5e Mon Sep 17 00:00:00 2001 From: Shreya Sharma Date: Thu, 8 Jan 2026 23:20:35 +0530 Subject: [PATCH 019/250] fixes --- src/core/friendly_errors/param_validator.js | 82 +++++++++++++-------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/src/core/friendly_errors/param_validator.js b/src/core/friendly_errors/param_validator.js index 790b470168..6ca8fc4362 100644 --- a/src/core/friendly_errors/param_validator.js +++ b/src/core/friendly_errors/param_validator.js @@ -64,7 +64,7 @@ function validateParams(p5, fn, lifecycles) { 'Boolean': z.boolean(), 'Function': z.function(), 'Integer': z.number().int(), - 'Number': z.number(), + 'Number': z.union([z.number(), z.literal(Infinity), z.literal(-Infinity)]), 'Object': z.object({}), 'String': z.string() }; @@ -415,46 +415,70 @@ function validateParams(p5, fn, lifecycles) { const expectedTypes = new Set(); let actualType; - error.errors.forEach(err => { - const issue = err[0]; - if (issue) { + let sawNumber = false; + let sawInfinityLiteral = false; + let sawNonInfinityConstant = false; + + const flattenIssues = iss => { + if (!iss) return []; + if (iss.code === 'invalid_union') { + const subs = iss.unionErrors?.flatMap(u => u.issues) || []; + return subs.flatMap(si => flattenIssues(si[0] || si)); + } + return [iss]; + }; + + const flat = (error.errors || []).flatMap(e => flattenIssues(e[0])); + + for (const issue of flat) { + if (!issue) continue; + + if (issue.code === 'invalid_type') { + if (issue.expected === 'number') sawNumber = true; if (!actualType) { - actualType = issue.message; + const rec = issue.message?.split(', received ')[1]; + if (rec) actualType = rec; } + continue; + } - if (issue.code === 'invalid_type') { - actualType = issue.message.split(', received ')[1]; - expectedTypes.add(issue.expected); - } - // The case for constants. Since we don't want to print out the actual - // constant values in the error message, the error message will - // direct users to the documentation. - else if (issue.code === 'invalid_value') { - expectedTypes.add('constant (please refer to documentation for allowed values)'); - actualType = args[error.path[0]]; - } else if (issue.code === 'custom') { - const match = issue.message.match(/Input not instance of (\w+)/); - if (match) expectedTypes.add(match[1]); - actualType = undefined; + if (issue.code === 'invalid_literal' || issue.code === 'invalid_value') { + const exp = 'expected' in issue ? issue.expected : undefined; + if (exp === Infinity || exp === -Infinity) { + sawInfinityLiteral = true; + } else { + sawNonInfinityConstant = true; } + if (!actualType) actualType = args[error.path[0]]; + continue; } - }); + + if (issue.code === 'custom') { + const m = issue.message?.match(/Input not instance of (\w+)/); + if (m) expectedTypes.add(m[1]); + actualType = undefined; + continue; + } + } + + if (sawNumber) expectedTypes.add('number'); + if (sawNonInfinityConstant) { + expectedTypes.add('constant (please refer to documentation for allowed values)'); + } + + if (sawNumber && sawInfinityLiteral && !sawNonInfinityConstant) { + expectedTypes.delete('constant (please refer to documentation for allowed values)'); + } if (expectedTypes.size > 0) { - if (error.path?.length > 0 && args[error.path[0]] instanceof Promise) { - message += 'Did you mean to put `await` before a loading function? ' + - 'An unexpected Promise was found. '; + if (error.path?.length > 0 && args[error.path[0]] instanceof Promise) { + message += 'Did you mean to put `await` before a loading function? An unexpected Promise was found. '; isVersionError = true; } - const expectedTypesStr = Array.from(expectedTypes).join(' or '); const position = error.path.join('.'); - - message += buildTypeMismatchMessage( - actualType, expectedTypesStr, position - ); + message += buildTypeMismatchMessage(actualType, expectedTypesStr, position); } - return message; }; From cf1f8c3366aa21c7d21c6fc00f6a3f4cf6c0ccd6 Mon Sep 17 00:00:00 2001 From: Perminder Singh <127239756+perminder-17@users.noreply.github.com> Date: Sun, 11 Jan 2026 20:57:03 +0530 Subject: [PATCH 020/250] Refactor error flattening and type detection --- src/core/friendly_errors/param_validator.js | 34 ++++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/core/friendly_errors/param_validator.js b/src/core/friendly_errors/param_validator.js index 6ca8fc4362..6075b17fff 100644 --- a/src/core/friendly_errors/param_validator.js +++ b/src/core/friendly_errors/param_validator.js @@ -428,13 +428,23 @@ function validateParams(p5, fn, lifecycles) { return [iss]; }; - const flat = (error.errors || []).flatMap(e => flattenIssues(e[0])); + const flat = (error.errors || []).flatMap(e => + (Array.isArray(e) ? e : [e]).flatMap(i => flattenIssues(i)) + ); + + const typeOfArg = val => { + if (val === null) return 'null'; + if (Array.isArray(val)) return 'array'; + if (typeof val === 'number' && Number.isNaN(val)) return 'NaN'; + return typeof val; + }; for (const issue of flat) { if (!issue) continue; if (issue.code === 'invalid_type') { if (issue.expected === 'number') sawNumber = true; + if (issue.expected) expectedTypes.add(issue.expected); if (!actualType) { const rec = issue.message?.split(', received ')[1]; if (rec) actualType = rec; @@ -443,13 +453,21 @@ function validateParams(p5, fn, lifecycles) { } if (issue.code === 'invalid_literal' || issue.code === 'invalid_value') { - const exp = 'expected' in issue ? issue.expected : undefined; - if (exp === Infinity || exp === -Infinity) { - sawInfinityLiteral = true; - } else { - sawNonInfinityConstant = true; - } - if (!actualType) actualType = args[error.path[0]]; + const values = Array.isArray(issue.values) + ? issue.values + : ('expected' in issue ? [issue.expected] : []); + + if (values.some(v => v === Infinity || v === -Infinity)) { + sawInfinityLiteral = true; + } + + if (values.some(v => v !== Infinity && v !== -Infinity)) { + sawNonInfinityConstant = true; + } + + const idx = issue.path?.[0] ?? error.path?.[0]; + if (!actualType && idx !== undefined) actualType = typeOfArg(args[idx]); + continue; } From 0f5a475d45e99ed048a29366972a572a610f029f Mon Sep 17 00:00:00 2001 From: Perminder Singh <127239756+perminder-17@users.noreply.github.com> Date: Mon, 12 Jan 2026 00:26:58 +0530 Subject: [PATCH 021/250] Adding more conditions for fixing --- src/core/friendly_errors/param_validator.js | 23 +++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/core/friendly_errors/param_validator.js b/src/core/friendly_errors/param_validator.js index 6075b17fff..1e77ec88db 100644 --- a/src/core/friendly_errors/param_validator.js +++ b/src/core/friendly_errors/param_validator.js @@ -422,8 +422,15 @@ function validateParams(p5, fn, lifecycles) { const flattenIssues = iss => { if (!iss) return []; if (iss.code === 'invalid_union') { - const subs = iss.unionErrors?.flatMap(u => u.issues) || []; - return subs.flatMap(si => flattenIssues(si[0] || si)); + const subsV3 = iss.unionErrors?.flatMap(u => u.issues) || []; + const subsV4 = iss.errors || []; + + const flatV3 = subsV3.flatMap(si => flattenIssues(si[0] || si)); + const flatV4 = subsV4.flatMap(e => + (Array.isArray(e) ? e : [e]).flatMap(i => flattenIssues(i)) + ); + + return [...flatV3, ...flatV4]; } return [iss]; }; @@ -493,9 +500,17 @@ function validateParams(p5, fn, lifecycles) { message += 'Did you mean to put `await` before a loading function? An unexpected Promise was found. '; isVersionError = true; } - const expectedTypesStr = Array.from(expectedTypes).join(' or '); + let expectedArr = Array.from(expectedTypes); + if (expectedTypes.has('number')) { + expectedArr = ['number', ...expectedArr.filter(t => t !== 'number')]; + } + const expectedTypesStr = expectedArr.join(' or '); + const position = error.path.join('.'); - message += buildTypeMismatchMessage(actualType, expectedTypesStr, position); + const idx = error.path?.[0]; + const received = + sawNonInfinityConstant && idx !== undefined ? args[idx] : actualType; + message += buildTypeMismatchMessage(received, expectedTypesStr, position); } return message; }; From 255af5a3f1cc5a311db486cdafbf7048a10f3af4 Mon Sep 17 00:00:00 2001 From: dhowe Date: Sun, 1 Feb 2026 13:02:14 +0000 Subject: [PATCH 022/250] remove getters for matrix transforms: shear, rotate, translate, scale --- docs/parameterData.json | 21 +++++++-------------- src/core/p5.Renderer2D.js | 19 ------------------- src/core/transform.js | 16 ---------------- 3 files changed, 7 insertions(+), 49 deletions(-) diff --git a/docs/parameterData.json b/docs/parameterData.json index 199d8f66c1..bb83f10368 100644 --- a/docs/parameterData.json +++ b/docs/parameterData.json @@ -534,32 +534,28 @@ [ "Number", "p5.Vector|Number[]?" - ], - [] + ] ] }, "rotateX": { "overloads": [ [ "Number" - ], - [] + ] ] }, "rotateY": { "overloads": [ [ "Number" - ], - [] + ] ] }, "rotateZ": { "overloads": [ [ "Number" - ], - [] + ] ] }, "scale": { @@ -571,24 +567,21 @@ ], [ "p5.Vector|Number[]" - ], - [] + ] ] }, "shearX": { "overloads": [ [ "Number" - ], - [] + ] ] }, "shearY": { "overloads": [ [ "Number" - ], - [] + ] ] }, "translate": { diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 6012751ad2..e95d81e93d 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -1030,26 +1030,11 @@ class Renderer2D extends Renderer { } rotate(rad) { - if (typeof rad === 'undefined') { - const matrix = this.drawingContext.getTransform(); - let angle = this._pInst.decomposeMatrix(matrix).rotation; - if (angle < 0) { - angle += Math.PI * 2; // ensure a positive angle - } - if (this._pInst._angleMode === this._pInst.DEGREES) { - angle *= constants.RAD_TO_DEG; // to degrees - } - return Math.abs(angle); - } this.drawingContext.rotate(rad); return this; } scale(x, y) { - if (typeof x === 'undefined' && typeof y === 'undefined') { - const matrix = this.drawingContext.getTransform(); - return this._pInst.decomposeMatrix(matrix).scale; - } // support passing objects with x,y properties (including p5.Vector) if (typeof x === 'object' && 'x' in x && 'y' in x) { y = x.y; @@ -1060,10 +1045,6 @@ class Renderer2D extends Renderer { } translate(x, y) { - if (typeof x === 'undefined' && typeof y === 'undefined') { - const matrix = this.drawingContext.getTransform(); - return this._pInst.decomposeMatrix(matrix).translation; - } // support passing objects with x,y properties (including p5.Vector) if (typeof x === 'object' && 'x' in x && 'y' in x) { y = x.y; diff --git a/src/core/transform.js b/src/core/transform.js index 133a9da9cc..e5ab7277ac 100644 --- a/src/core/transform.js +++ b/src/core/transform.js @@ -1131,14 +1131,6 @@ function transform(p5, fn){ */ fn.shearX = function(angle) { // p5._validateParameters('shearX', arguments); - if (typeof angle === 'undefined') { - let matrix = this._renderer.drawingContext.getTransform(); - let rad = this.decomposeMatrix(matrix).shear.x; - if (fn._angleMode === fn.DEGREES) { - rad *= fn.RAD_TO_DEG; // to degrees - } - return rad; - } const rad = this._toRadians(angle); this._renderer.applyMatrix(1, 0, Math.tan(rad), 1, 0, 0); return this; @@ -1216,14 +1208,6 @@ function transform(p5, fn){ */ fn.shearY = function(angle) { // p5._validateParameters('shearY', arguments); - if (typeof angle === 'undefined') { - let matrix = this._renderer.drawingContext.getTransform(); - let rad = this.decomposeMatrix(matrix).shear.y; - if (fn._angleMode === fn.DEGREES) { - rad *= fn.RAD_TO_DEG; // to degrees - } - return rad; - } const rad = this._toRadians(angle); this._renderer.applyMatrix(1, Math.tan(rad), 0, 1, 0, 0); return this; From f26eff61d6e6a41c11f7dd7dcf28f9a335465888 Mon Sep 17 00:00:00 2001 From: dhowe Date: Sun, 1 Feb 2026 13:15:41 +0000 Subject: [PATCH 023/250] remove translate, rotate, scale from getter tests --- src/image/loading_displaying.js | 2 ++ test/unit/core/properties.js | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 9617b2ce3c..fb914ff379 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -1296,6 +1296,7 @@ function loadingDisplaying(p5, fn){ } const c = this.color(...args); this._renderer.states.setValue('tint', c._getRGBA([255, 255, 255, 255])); + return this; }; /** @@ -1333,6 +1334,7 @@ function loadingDisplaying(p5, fn){ */ fn.noTint = function() { this._renderer.states.setValue('tint', null); + return this; }; /** diff --git a/test/unit/core/properties.js b/test/unit/core/properties.js index 10bcce9162..34bb499114 100644 --- a/test/unit/core/properties.js +++ b/test/unit/core/properties.js @@ -35,9 +35,9 @@ suite('Set/get properties', function() { pixelDensity: 1, cursor: 'pointer', - rotate: p.PI, - translate: { x: 1, y: 2 }, - scale: { x: 1, y: 2 }, + // rotate: p.PI, + // translate: { x: 1, y: 2 }, + // scale: { x: 1, y: 2 }, bezierOrder: 2, splineProperties: { ends: p.EXCLUDE, tightness: -5 }, textAlign: { horizontal: p.CENTER, vertical: p.CENTER }, From be4de9813a83456ba4599486743830498a690f6a Mon Sep 17 00:00:00 2001 From: dhowe Date: Sun, 1 Feb 2026 14:15:21 +0000 Subject: [PATCH 024/250] re-implemented tint, plus textureWrap and textureMode --- src/color/p5.Color.js | 2 +- src/core/p5.Renderer2D.js | 4 +++- src/image/loading_displaying.js | 6 ++++-- src/webgl/material.js | 21 +++++++++++++++++++-- test/unit/core/properties.js | 17 ++++++++++++----- 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/color/p5.Color.js b/src/color/p5.Color.js index 3622bb4b9c..fb901e9ec4 100644 --- a/src/color/p5.Color.js +++ b/src/color/p5.Color.js @@ -675,7 +675,7 @@ class Color { if(!Array.isArray(v)){ return [0, v]; }else{ - return v + return v; } }); diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index e95d81e93d..87a008009c 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -167,7 +167,8 @@ class Renderer2D extends Renderer { background(...args) { if (args.length === 0) { - return this.states.background; // getter (#8278) + return this;// setter with no args does nothing + //return this.states.background; // getter (#8278) } let bgForState = null; this.push(); @@ -207,6 +208,7 @@ class Renderer2D extends Renderer { this.pop(); this.states.setValue('background', bgForState); // set state (#8278) + return this; } clear() { diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index fb914ff379..d080e2fb4a 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -1294,8 +1294,10 @@ function loadingDisplaying(p5, fn){ if (args.length === 0) { return this.color(this._renderer.states.tint); // getter } - const c = this.color(...args); - this._renderer.states.setValue('tint', c._getRGBA([255, 255, 255, 255])); + if (args && args.length) { + const c = this.color(...args); + this._renderer.states.setValue('tint', c._getRGBA([255, 255, 255, 255])); + } return this; }; diff --git a/src/webgl/material.js b/src/webgl/material.js index 60f01a3969..ed4a1b7a99 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -2563,6 +2563,9 @@ function material(p5, fn){ * */ fn.textureMode = function (mode) { + if (typeof mode === 'undefined') { // getter + return this._renderer.states.textureMode; + } if (mode !== constants.IMAGE && mode !== constants.NORMAL) { console.warn( `You tried to set ${mode} textureMode only supports IMAGE & NORMAL ` @@ -2839,12 +2842,26 @@ function material(p5, fn){ * */ fn.textureWrap = function (wrapX, wrapY = wrapX) { + if (typeof wrapX === 'undefined') { // getter + return { + x: this._renderer.states.textureWrapX, + y: this._renderer.states.textureWrapY + }; + } + // accept what is returned from the getter + if (wrapX.hasOwnProperty('x') && wrapX.hasOwnProperty('y')) { + wrapX = wrapX.x; + wrapY = wrapX.y; + } this._renderer.states.setValue('textureWrapX', wrapX); this._renderer.states.setValue('textureWrapY', wrapY); - for (const texture of this._renderer.textures.values()) { - texture.setWrapMode(wrapX, wrapY); + if (this._renderer.textures) { + for (const texture of this._renderer.textures.values()) { + texture.setWrapMode(wrapX, wrapY); + } } + return this; }; /** diff --git a/test/unit/core/properties.js b/test/unit/core/properties.js index 34bb499114..04e44b3978 100644 --- a/test/unit/core/properties.js +++ b/test/unit/core/properties.js @@ -18,7 +18,6 @@ suite('Set/get properties', function() { });*/ let getters = { - background: new p5.Color([100, 100, 50]), fill: new p5.Color([100, 200, 50]), stroke: new p5.Color([200, 100, 50, 100]), tint: new p5.Color([100, 140, 50]), @@ -28,6 +27,7 @@ suite('Set/get properties', function() { blendMode: 'source-over', imageMode: p.CORNER, ellipseMode: p.CORNER, + angleMode: p.DEGREES, strokeWeight: 6, strokeCap: p.ROUND, @@ -35,11 +35,12 @@ suite('Set/get properties', function() { pixelDensity: 1, cursor: 'pointer', - // rotate: p.PI, - // translate: { x: 1, y: 2 }, - // scale: { x: 1, y: 2 }, bezierOrder: 2, splineProperties: { ends: p.EXCLUDE, tightness: -5 }, + + textureMode: p.IMAGE, // 3D only + textureWrap: { x: p.REPEAT, y: p.MIRROR }, // 3D only + textAlign: { horizontal: p.CENTER, vertical: p.CENTER }, textLeading: 18, textFont: 'arial', @@ -48,6 +49,12 @@ suite('Set/get properties', function() { textWrap: p.WORD, textDirection: 1, textWeight: 1 + + // rotate: p.PI, see #8278 + // translate: { x: 1, y: 2 }, + // scale: { x: 1, y: 2 }, + // background: new p5.Color([100, 100, 50]), + }; Object.keys(getters).forEach(prop => { @@ -62,7 +69,7 @@ suite('Set/get properties', function() { p[prop](...arg); // set with array } else { - p[prop](arg); // set with primitive + p[prop](arg); // set with primitive or p5.Color } // getter assert.strictEqual(p[prop]().toString(), arg.toString(), `${arg.toString()}`); From f87df11565fce36d530950d5a9e2a37f0927f763 Mon Sep 17 00:00:00 2001 From: dhowe Date: Sun, 1 Feb 2026 14:17:09 +0000 Subject: [PATCH 025/250] update fes data --- docs/parameterData.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/parameterData.json b/docs/parameterData.json index bb83f10368..6de82636f3 100644 --- a/docs/parameterData.json +++ b/docs/parameterData.json @@ -162,7 +162,6 @@ }, "background": { "overloads": [ - [], [ "p5.Color" ], @@ -2969,7 +2968,8 @@ "overloads": [ [ "IMAGE|NORMAL" - ] + ], + [] ] }, "textureWrap": { @@ -2977,7 +2977,8 @@ [ "CLAMP|REPEAT|MIRROR", "CLAMP|REPEAT|MIRROR?" - ] + ], + [] ] }, "normalMaterial": { From 492b9e9fbe39cd5a3025b0c8d7a281900a900cc0 Mon Sep 17 00:00:00 2001 From: ksen0 Date: Fri, 6 Feb 2026 15:16:52 +0100 Subject: [PATCH 026/250] Vector decorator does not use Proxy --- src/math/patch-vector.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/math/patch-vector.js b/src/math/patch-vector.js index ec2ed993e8..40fc2d6f80 100644 --- a/src/math/patch-vector.js +++ b/src/math/patch-vector.js @@ -2,10 +2,9 @@ export default function patchVector(p5, fn, lifecycles){ p5.decorateHelper('createVector', function(target){ return function(...args){ if(args.length === 0){ - // console.log('empty call to createVector'); - return new Proxy(new p5.Vector(0, 0, 0), { + return new p5.Vector(0, 0, 0), { - }); + }; }else{ return target.call(this, ...args); } From c138df8233faff871228c8a64b20e5f11625d635 Mon Sep 17 00:00:00 2001 From: anshumancanrock Date: Wed, 28 Jan 2026 19:19:11 +0530 Subject: [PATCH 027/250] docs: add documentation for mix() method in p5.strands --- src/strands/p5.strands.js | 134 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 7d212c332d..761daf6d02 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -709,3 +709,137 @@ if (typeof p5 !== "undefined") { * @method getCameraInputs * @param {Function} callback */ + +/** + * Performs linear interpolation between two values. + * + * The `mix()` function linearly interpolates between two values based on a third + * parameter. It's a GLSL built-in function available in p5.strands shaders. + * + * The function computes: `x * (1 - a) + y * a` + * + * When `a` is 0.0, the function returns `x`. When `a` is 1.0, it returns `y`. + * Values between 0.0 and 1.0 produce a linear blend between the two values. + * + * This function works with scalars, vectors (vec2, vec3, vec4), and can also + * accept a boolean for the third parameter to select between the two values. + * + * Note: This function is only available inside shader code created with + * buildMaterialShader(), + * buildColorShader(), or similar functions. + * For regular p5.js code, use lerp() instead. + * + * @method mix + * @param {Number|p5.Vector} x first value to interpolate from. + * @param {Number|p5.Vector} y second value to interpolate to. + * @param {Number|Boolean} a interpolation amount (0.0-1.0) or boolean selector. + * @return {Number|p5.Vector} interpolated value. + * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = buildMaterialShader(applyMix); + * describe('A sphere that transitions smoothly between red and blue.'); + * } + * + * function applyMix() { + * let factor = uniformFloat(); + * + * pixelInputs.begin(); + * // Mix between red and blue based on factor + * let red = vec3(1, 0, 0); + * let blue = vec3(0, 0, 1); + * let mixedColor = mix(red, blue, factor); + * pixelInputs.color = vec4(mixedColor, 1); + * // Set ambient color to match to avoid default ambient lighting + * pixelInputs.ambientColor = pixelInputs.color.rgb; + * pixelInputs.end(); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * // Oscillate factor between 0 and 1 + * let factor = (sin(frameCount * 0.02) + 1) / 2; + * myShader.setUniform('factor', factor); + * noStroke(); + * sphere(80); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = buildMaterialShader(positionMix); + * describe('A sphere with vertices that blend between two positions.'); + * } + * + * function positionMix() { + * let time = uniformFloat(); + * + * worldInputs.begin(); + * // Blend vertex position between original and modified + * let originalPos = worldInputs.position; + * let modifiedPos = originalPos + vec3(0, sin(time * 0.001) * 20, 0); + * let factor = (sin(worldInputs.position.x * 0.1) + 1) / 2; + * worldInputs.position = mix(originalPos, modifiedPos, factor); + * worldInputs.end(); + * } + * + * function draw() { + * background(220); + * shader(myShader); + * myShader.setUniform('time', millis()); + * lights(); + * noStroke(); + * fill('red'); + * sphere(70); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = buildMaterialShader(gradientMix); + * describe('A torus with a color gradient created using mix().'); + * } + * + * function gradientMix() { + * pixelInputs.begin(); + * // Create a gradient based on texture coordinates + * let gradient = pixelInputs.texCoord.x; + * let color1 = vec3(1, 0.5, 0); // Orange + * let color2 = vec3(0.5, 0, 1); // Purple + * let mixedColor = mix(color1, color2, gradient); + * pixelInputs.color = vec4(mixedColor, 1); + * // Set ambient color to match to avoid default ambient lighting + * pixelInputs.ambientColor = pixelInputs.color.rgb; + * pixelInputs.end(); + * } + * + * function draw() { + * background(200); + * shader(myShader); + * lights(); + * noStroke(); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * torus(60, 20); + * } + * + *
+ */ From b54a299722c9a53946eab6ad0ac209fe64299e89 Mon Sep 17 00:00:00 2001 From: Shreya Sharma Date: Thu, 12 Feb 2026 00:33:28 +0530 Subject: [PATCH 028/250] removing redundant code --- src/core/friendly_errors/param_validator.js | 121 ++++++-------------- 1 file changed, 38 insertions(+), 83 deletions(-) diff --git a/src/core/friendly_errors/param_validator.js b/src/core/friendly_errors/param_validator.js index 1e77ec88db..af88350b27 100644 --- a/src/core/friendly_errors/param_validator.js +++ b/src/core/friendly_errors/param_validator.js @@ -411,110 +411,65 @@ function validateParams(p5, fn, lifecycles) { // of any of them. In this case, aggregate all possible types and print // a friendly error message that indicates what the expected types are at // which position (position is not 0-indexed, for accessibility reasons). + const processUnionError = error => { const expectedTypes = new Set(); let actualType; - let sawNumber = false; - let sawInfinityLiteral = false; - let sawNonInfinityConstant = false; - - const flattenIssues = iss => { - if (!iss) return []; - if (iss.code === 'invalid_union') { - const subsV3 = iss.unionErrors?.flatMap(u => u.issues) || []; - const subsV4 = iss.errors || []; - - const flatV3 = subsV3.flatMap(si => flattenIssues(si[0] || si)); - const flatV4 = subsV4.flatMap(e => - (Array.isArray(e) ? e : [e]).flatMap(i => flattenIssues(i)) - ); - - return [...flatV3, ...flatV4]; + const collectIssue = issue => { + if (!issue) return; + if (!actualType) { + actualType = issue.message; } - return [iss]; - }; - - const flat = (error.errors || []).flatMap(e => - (Array.isArray(e) ? e : [e]).flatMap(i => flattenIssues(i)) - ); - - const typeOfArg = val => { - if (val === null) return 'null'; - if (Array.isArray(val)) return 'array'; - if (typeof val === 'number' && Number.isNaN(val)) return 'NaN'; - return typeof val; - }; - - for (const issue of flat) { - if (!issue) continue; if (issue.code === 'invalid_type') { - if (issue.expected === 'number') sawNumber = true; - if (issue.expected) expectedTypes.add(issue.expected); - if (!actualType) { - const rec = issue.message?.split(', received ')[1]; - if (rec) actualType = rec; - } - continue; + actualType = issue.message.split(', received ')[1]; + expectedTypes.add(issue.expected); } - - if (issue.code === 'invalid_literal' || issue.code === 'invalid_value') { - const values = Array.isArray(issue.values) - ? issue.values - : ('expected' in issue ? [issue.expected] : []); - - if (values.some(v => v === Infinity || v === -Infinity)) { - sawInfinityLiteral = true; - } - - if (values.some(v => v !== Infinity && v !== -Infinity)) { - sawNonInfinityConstant = true; - } - - const idx = issue.path?.[0] ?? error.path?.[0]; - if (!actualType && idx !== undefined) actualType = typeOfArg(args[idx]); - - continue; - } - - if (issue.code === 'custom') { - const m = issue.message?.match(/Input not instance of (\w+)/); - if (m) expectedTypes.add(m[1]); + // The case for constants. Since we don't want to print out the actual + // constant values in the error message, the error message will + // direct users to the documentation. + else if (issue.code === 'invalid_value') { + if (Array.isArray(issue.values) && issue.values.every(v => v === Infinity || v === -Infinity)) { + expectedTypes.add('number'); + } else { + expectedTypes.add('constant (please refer to documentation for allowed values)'); + actualType = args[error.path[0]]; + } + } else if (issue.code === 'custom') { + const match = issue.message.match(/Input not instance of (\w+)/); + if (match) expectedTypes.add(match[1]); actualType = undefined; - continue; + } else if (issue.code === 'invalid_union') { + issue.errors.forEach(nestedErr => { + nestedErr.forEach(nestedIssue => collectIssue(nestedIssue)); + }); } - } - - if (sawNumber) expectedTypes.add('number'); - if (sawNonInfinityConstant) { - expectedTypes.add('constant (please refer to documentation for allowed values)'); - } + }; - if (sawNumber && sawInfinityLiteral && !sawNonInfinityConstant) { - expectedTypes.delete('constant (please refer to documentation for allowed values)'); - } + error.errors.forEach(err => { + err.forEach(issue => collectIssue(issue)); + }); if (expectedTypes.size > 0) { - if (error.path?.length > 0 && args[error.path[0]] instanceof Promise) { - message += 'Did you mean to put `await` before a loading function? An unexpected Promise was found. '; + if (error.path?.length > 0 && args[error.path[0]] instanceof Promise) { + message += 'Did you mean to put `await` before a loading function? ' + + 'An unexpected Promise was found. '; isVersionError = true; } - let expectedArr = Array.from(expectedTypes); - if (expectedTypes.has('number')) { - expectedArr = ['number', ...expectedArr.filter(t => t !== 'number')]; - } - const expectedTypesStr = expectedArr.join(' or '); + const expectedTypesStr = Array.from(expectedTypes).join(' or '); const position = error.path.join('.'); - const idx = error.path?.[0]; - const received = - sawNonInfinityConstant && idx !== undefined ? args[idx] : actualType; - message += buildTypeMismatchMessage(received, expectedTypesStr, position); + + message += buildTypeMismatchMessage( + actualType, expectedTypesStr, position + ); } + return message; }; + switch (currentError.code) { case 'invalid_union': { processUnionError(currentError); From 93920ba50a9e85677955154ea685409cdeb802af Mon Sep 17 00:00:00 2001 From: ksen0 Date: Fri, 6 Feb 2026 17:05:51 +0100 Subject: [PATCH 029/250] Abstract out vector format and dimension checks for binary vector ops --- src/math/math.js | 5 - src/math/p5.Vector.js | 282 +++++++++++++----------------------- src/math/patch-vector.js | 11 +- test/unit/math/p5.Vector.js | 8 +- 4 files changed, 115 insertions(+), 191 deletions(-) diff --git a/src/math/math.js b/src/math/math.js index 9071aa233b..9f2e110861 100644 --- a/src/math/math.js +++ b/src/math/math.js @@ -100,11 +100,6 @@ function math(p5, fn) { * */ fn.createVector = function (x, y, z) { - if (arguments.length === 0) { - p5._friendlyError( - 'In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector.' - ); - } if (this instanceof p5) { return new p5.Vector( this._fromRadians.bind(this), diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index e3ade52617..bfd4b517ff 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -5,31 +5,34 @@ import * as constants from '../core/constants'; -/// HELPERS FOR REMAINDER METHOD -const calculateRemainder2D = function (xComponent, yComponent) { - if (xComponent !== 0) { - this.x = this.x % xComponent; - } - if (yComponent !== 0) { - this.y = this.y % yComponent; - } - return this; -}; - -const calculateRemainder3D = function (xComponent, yComponent, zComponent) { - if (xComponent !== 0) { - this.x = this.x % xComponent; - } - if (yComponent !== 0) { - this.y = this.y % yComponent; - } - if (zComponent !== 0) { - this.z = this.z % zComponent; +/// HELPER FOR SMALLER DIMENSION PRIORITY LOGIC. +/// Pending implementation as decorator. +const smallerDimensionPriorityHelper = function(dimOther, dimSelf) { + const resultDimension = Math.min(dimOther, dimSelf); + if (dimOther != dimSelf) { + + console.warn( + 'When working with two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as ' + resultDimension + 'D vectors, and any additional values of the linger vector will be ignored.', + ); } - return this; -}; + return resultDimension +} class Vector { + /** + * The values of the N-dimensional vector. + * + * This array of numbers that represents the vector. + * Each number in the array corresponds to a different component of the vector, + * like its position in different directions (e.g., x, y, z). + * + * You can update the values of the entire vector to a new set of values. + * You need to provide an array of numbers, where each number represents a component + * of the vector (e.g., x, y, z). The length of the array will become the number of + * dimensions of the vector. + * + * @type {Array} The array of values representing the vector. + */ values = []; // This is how it comes in with createVector() @@ -41,45 +44,19 @@ class Vector { this._toRadians = args[1]; args = args.slice(2); } + + // TODO Implement using decorator API to reduce duplication. + // Should use the same check as patchVector on 'createVector' + if(args.length === 0){ + console.warn( + 'In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector.' + ); + args = [0, 0, 0] + } + this.values = args; } - // /** - // * Gets the values of the N-dimensional vector. - // * - // * This method returns an array of numbers that represent the vector. - // * Each number in the array corresponds to a different component of the vector, - // * like its position in different directions (e.g., x, y, z). - // * - // * @returns {Array} The array of values representing the vector. - // */ - // get values() { - // return this._values; - // } - - // /** - // * Sets the values of the vector. - // * - // * This method allows you to update the entire vector with a new set of values. - // * You need to provide an array of numbers, where each number represents a component - // * of the vector (e.g., x, y, z). The length of the array should match the number of - // * dimensions of the vector. If the array is shorter, the missing components will be - // * set to 0. If the array is longer, the extra values will be ignored. - // * - // * @param {Array} newValues - An array of numbers representing the new values for the vector. - // * - // */ - // set values(newValues) { - // let dimensions = newValues.length; - // if (dimensions === 0) { - // this.dimensions = 2; - // this._values = [0, 0, 0]; - // } else { - // this.dimensions = dimensions; - // this._values = newValues.slice(); - // } - // } - get dimensions(){ return this.values.length; } @@ -377,6 +354,8 @@ class Vector { } } + + /** * Adds to a vector's components. * @@ -512,13 +491,17 @@ class Vector { * @chainable */ add(...args) { + + // TODO Implement using decorator API to reduce duplication. if (args[0] instanceof Vector) { args = args[0].values; } else if (Array.isArray(args[0])) { args = args[0]; + } else if (args.length === 0) { + return this; } - const resultDimension = Math.min(args.length, this.dimensions); + const resultDimension = smallerDimensionPriorityHelper(args.length, this.dimensions); this.values = this.values.reduce((acc, v, i) => { if(i < resultDimension) acc[i] = this.values[i] + args[i]; return acc; @@ -527,6 +510,8 @@ class Vector { return this; } + + /** * Performs modulo (remainder) division with a vector's `x`, `y`, and `z` * components. @@ -648,73 +633,37 @@ class Vector { * @chainable */ rem(...args) { + + // TODO Implement using decorator API to reduce duplication. + if (args[0] instanceof Vector) { args = args[0].values; } else if (Array.isArray(args[0])) { args = args[0]; + } else if (args.length === 1) { + args = new Array(this.dimensions).fill(args[0]); + } else if (args.length === 0) { + return this; } - if(!args.every(v => v !== 0 && Number.isFinite(v))) return this; + if(!args.every(v => Number.isFinite(v))){ + console.warn( + 'p5.Vector.prototype.rem', + 'Arguments contain non-finite numbers' + ); + return this; + }; + + const resultDimension = smallerDimensionPriorityHelper(args.length, this.dimensions); - const resultDimension = Math.min(args.length, this.dimensions); this.values = this.values.reduce((acc, v, i) => { - if(i < resultDimension) acc[i] = this.values[i] % args[i]; + // Extra check for non empty operand + if(i < resultDimension && args[i] > 0) acc[i] = this.values[i] % args[i]; + else acc[i] = this.values[i] return acc; }, new Array(resultDimension)); return this; - // if (x instanceof Vector) { - // if ([x.x, x.y, x.z].every(Number.isFinite)) { - // const xComponent = parseFloat(x.x); - // const yComponent = parseFloat(x.y); - // const zComponent = parseFloat(x.z); - // return calculateRemainder3D.call( - // this, - // xComponent, - // yComponent, - // zComponent - // ); - // } - // } else if (Array.isArray(x)) { - // if (x.every(element => Number.isFinite(element))) { - // if (x.length === 2) { - // return calculateRemainder2D.call(this, x[0], x[1]); - // } - // if (x.length === 3) { - // return calculateRemainder3D.call(this, x[0], x[1], x[2]); - // } - // } - // } else if (arguments.length === 1) { - // if (Number.isFinite(arguments[0]) && arguments[0] !== 0) { - // this.x = this.x % arguments[0]; - // this.y = this.y % arguments[0]; - // this.z = this.z % arguments[0]; - // return this; - // } - // } else if (arguments.length === 2) { - // const vectorComponents = [...arguments]; - // if (vectorComponents.every(element => Number.isFinite(element))) { - // if (vectorComponents.length === 2) { - // return calculateRemainder2D.call( - // this, - // vectorComponents[0], - // vectorComponents[1] - // ); - // } - // } - // } else if (arguments.length === 3) { - // const vectorComponents = [...arguments]; - // if (vectorComponents.every(element => Number.isFinite(element))) { - // if (vectorComponents.length === 3) { - // return calculateRemainder3D.call( - // this, - // vectorComponents[0], - // vectorComponents[1], - // vectorComponents[2] - // ); - // } - // } - // } } /** @@ -847,13 +796,18 @@ class Vector { * @chainable */ sub(...args) { + + // TODO Implement using decorator API to reduce duplication. if (args[0] instanceof Vector) { args = args[0].values; } else if (Array.isArray(args[0])) { args = args[0]; + } else if (args.length === 0) { + return this; } - const resultDimension = Math.min(args.length, this.dimensions); + const resultDimension = smallerDimensionPriorityHelper(args.length, this.dimensions); + this.values = this.values.reduce((acc, v, i) => { if(i < resultDimension) acc[i] = this.values[i] - args[i]; return acc; @@ -1056,15 +1010,28 @@ class Vector { * @chainable */ mult(...args) { + // TODO Implement using decorator API to reduce duplication. + if (args[0] instanceof Vector) { args = args[0].values; } else if (Array.isArray(args[0])) { args = args[0]; + } else if (args.length === 1) { + args = new Array(this.dimensions).fill(args[0]); + } else if (args.length === 0) { + return this; } - if(!args.every(v => v !== 0 && Number.isFinite(v))) return this; + if(!args.every(v => Number.isFinite(v))){ + console.warn( + 'p5.Vector.prototype.mult', + 'Arguments contain non-finite numbers' + ); + return this; + }; + + const resultDimension = smallerDimensionPriorityHelper(args.length, this.dimensions); - const resultDimension = Math.min(args.length, this.dimensions); this.values = this.values.reduce((acc, v, i) => { if(i < resultDimension) acc[i] = this.values[i] * args[i]; return acc; @@ -1306,79 +1273,35 @@ class Vector { * @chainable */ div(...args) { + + // TODO Implement using decorator API to reduce duplication. + if (args[0] instanceof Vector) { args = args[0].values; } else if (Array.isArray(args[0])) { args = args[0]; + } else if (args.length === 1) { + args = new Array(this.dimensions).fill(args[0]); + + } else if (args.length === 0) { + return this; } - if(!args.every(v => v !== 0 && Number.isFinite(v))) return this; + if(!args.every(v => typeof v === 'number' && v !== 0 && Number.isFinite(v))){ + console.warn( + 'p5.Vector.prototype.div:', + 'arguments contain components that are either 0 or not finite numbers' + ); + return this; + }; + + const resultDimension = smallerDimensionPriorityHelper(args.length, this.dimensions); - const resultDimension = Math.min(args.length, this.dimensions); this.values = this.values.reduce((acc, v, i) => { if(i < resultDimension) acc[i] = this.values[i] / args[i]; return acc; }, new Array(resultDimension)); - // if (args.length === 0) return this; - // // If passed a vector - // if (args.length === 1 && args[0] instanceof Vector) { - // const v = args[0]; - // if ( - // v.values.every( - // val => Number.isFinite(val) && typeof val === 'number' - // ) - // ) { - // if (v.values.some(val => val === 0)) { - // console.warn('p5.Vector.prototype.div:', 'divide by 0'); - // return this; - // } - // // this._values = this._values.map((val, i) => val / v.values[i]); - // for (let i = 0; i < v.values.length; i++) { - // if(!this.values[i]) this.values[i] = 0; - // this.values[i] /= v.values[i]; - // } - // } else { - // console.warn( - // 'p5.Vector.prototype.div:', - // 'vector contains components that are either undefined or not finite numbers' - // ); - // } - // return this; - // } - - // // If passed an array - // if (args.length === 1 && Array.isArray(args[0])) { - // const arr = args[0]; - // if (arr.every(val => Number.isFinite(val) && typeof val === 'number')) { - // if (arr.some(val => val === 0)) { - // console.warn('p5.Vector.prototype.div:', 'divide by 0'); - // return this; - // } - // this.values = this.values.map((val, i) => val / arr[i]); - // } else { - // console.warn( - // 'p5.Vector.prototype.div:', - // 'array contains components that are either undefined or not finite numbers' - // ); - // } - // return this; - // } - - // // If passed individual arguments - // if (args.every(val => Number.isFinite(val) && typeof val === 'number')) { - // if (args.some(val => val === 0)) { - // console.warn('p5.Vector.prototype.div:', 'divide by 0'); - // return this; - // } - // this.values = this.values.map((val, i) => val / args[0]); - // } else { - // console.warn( - // 'p5.Vector.prototype.div:', - // 'arguments contain components that are either undefined or not finite numbers' - // ); - // } - return this; } @@ -1436,7 +1359,8 @@ class Vector { * // Create a p5.Vector object. * let p = createVector(30, 40); * - * // Draw a line from the origin. + * // Draw a line from th + * e origin. * line(0, 0, p.x, p.y); * * // Style the text. diff --git a/src/math/patch-vector.js b/src/math/patch-vector.js index 40fc2d6f80..fb5717d59d 100644 --- a/src/math/patch-vector.js +++ b/src/math/patch-vector.js @@ -1,13 +1,18 @@ export default function patchVector(p5, fn, lifecycles){ + + // An empty vector defaults to a 3D vector. p5.decorateHelper('createVector', function(target){ return function(...args){ if(args.length === 0){ - return new p5.Vector(0, 0, 0), { - - }; + p5._friendlyError( + 'In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector.' + ); + return target.call(this, 0, 0, 0); }else{ return target.call(this, ...args); } }; }); + + } diff --git a/test/unit/math/p5.Vector.js b/test/unit/math/p5.Vector.js index 4028ee2dba..41863f9898 100644 --- a/test/unit/math/p5.Vector.js +++ b/test/unit/math/p5.Vector.js @@ -398,7 +398,7 @@ suite('p5.Vector', function () { suite('add()', function () { beforeEach(function () { - v = new mockP5.Vector(); + v = new mockP5.Vector(0, 0, 0); }); suite('with p5.Vector', function () { @@ -1370,7 +1370,7 @@ suite('p5.Vector', function () { expect(v.lerp()).to.eql(v); }); - // PEND: ADD BACK IN + // TODO PEND: ADD BACK IN // suite('with p5.Vector', function() { // test('should call lerp with 4 arguments', function() { // spyOn(v, 'lerp').andCallThrough(); @@ -1956,12 +1956,12 @@ suite('p5.Vector', function () { v = new mockP5.Vector(); }); - test('should set values to [0,0,0] if values array is empty', function () { + test('should NOT set values to [0,0,0] if values array is empty', function () { v.values = []; assert.equal(v.x, 0); assert.equal(v.y, 0); assert.equal(v.z, 0); - assert.equal(v.dimensions, 2); + assert.equal(v.dimensions, 0); }); }); suite('get value', function () { From 426929575d65c23b7a825a1bb2118d8ae0892521 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 15 Feb 2026 11:06:34 -0500 Subject: [PATCH 030/250] Initial compute shader support --- preview/index.html | 125 +++++++-- src/strands/ir_builders.js | 88 +++++++ src/strands/ir_types.js | 2 + src/strands/p5.strands.js | 2 + src/strands/strands_api.js | 31 ++- src/strands/strands_codegen.js | 11 +- src/strands/strands_node.js | 38 ++- src/webgl/p5.RendererGL.js | 1 + src/webgl/p5.Shader.js | 127 ++++++--- src/webgl/strands_glslBackend.js | 4 + src/webgpu/p5.RendererWebGPU.js | 416 +++++++++++++++++++++++++----- src/webgpu/shaders/compute.js | 42 +++ src/webgpu/strands_wgslBackend.js | 60 ++++- 13 files changed, 822 insertions(+), 125 deletions(-) create mode 100644 src/webgpu/shaders/compute.js diff --git a/preview/index.html b/preview/index.html index ed76b913f5..c4df32b6bd 100644 --- a/preview/index.html +++ b/preview/index.html @@ -32,6 +32,15 @@ let env; let instance; + // Compute shader variables + let computeShader; + let positionBuffer; + let velocityBuffer; + let bouncingCirclesShader; + let circleGeometry; + const NUM_CIRCLES = 10_000; + const RADIUS = 2; + p.setup = async function () { await p.createCanvas(400, 400, p.WEBGPU); env = await p.loadImage('img/spheremap.jpg'); @@ -41,6 +50,7 @@ fbo = p.createFramebuffer(); instance = p.buildGeometry(() => p.sphere(5)); + circleGeometry = p.buildGeometry(() => p.sphere(RADIUS)); redFilter = p.baseFilterShader().modify(() => { p.getColor((inputs, canvasContent) => { @@ -64,22 +74,10 @@ } tex.updatePixels(); fbo.draw(() => { - //p.clear(); - //p.background('orange'); p.imageMode(p.CENTER); p.image(tex, 0, 0, p.width, p.height); }); - /*sh = p.baseMaterialShader().modify({ - uniforms: { - 'f32 time': () => p.millis(), - }, - 'Vertex getWorldInputs': `(inputs: Vertex) { - var result = inputs; - result.position.y += 40.0 * sin(uniforms.time * 0.005); - return result; - }`, - })*/ sh = p.baseMaterialShader().modify(() => { const time = p.uniformFloat(() => p.millis()); p.getWorldInputs((inputs) => { @@ -93,19 +91,94 @@ return inputs; }); }, { p }) - /*ssh = p.baseStrokeShader().modify({ - uniforms: { - 'f32 time': () => p.millis(), - }, - 'StrokeVertex getWorldInputs': `(inputs: StrokeVertex) { - var result = inputs; - result.position.y += 40.0 * sin(uniforms.time * 0.005); - return result; - }`, - })*/ + + // Initialize storage buffers with random positions and velocities + const initialPositions = []; + const initialVelocities = []; + for (let i = 0; i < NUM_CIRCLES * 2; i += 2) { + // Random position (x, y) - store as pairs + initialPositions.push(p.random(-150, 150)); // x + initialPositions.push(p.random(-150, 150)); // y + + // Random velocity (vx, vy) - DVD logo style + initialVelocities.push(0.1 * p.random(1, 3) * (p.random() > 0.5 ? 1 : -1)); // vx + initialVelocities.push(0.1 * p.random(1, 3) * (p.random() > 0.5 ? 1 : -1)); // vy + } + + positionBuffer = p.createStorage(initialPositions); + velocityBuffer = p.createStorage(initialVelocities); + + // Create compute shader for physics simulation + computeShader = p.buildComputeShader(() => { + const positions = p.uniformStorage('positions'); + const velocities = p.uniformStorage('velocities'); + const bounds = p.uniformVec2(() => [p.width / 2 - RADIUS, p.height / 2 - RADIUS]); + const deltaTime = p.uniformFloat(() => p.deltaTime * 0.1); + + p.processData((inputs) => { + const idx = inputs.globalId.x; + + // Each circle has 2 floats (x, y) + const posX_idx = idx * 2; + const posY_idx = idx * 2 + 1; + + // Read current position and velocity + let posX = positions.get(posX_idx); + let posY = positions.get(posY_idx); + let velX = velocities.get(posX_idx); + let velY = velocities.get(posY_idx); + + // Update position + posX = posX + velX * deltaTime; + posY = posY + velY * deltaTime; + + // Bounce off boundaries (DVD logo style) + if (posX > bounds.x || posX < -bounds.x) { + velX = -velX; + posX = p.clamp(posX, -bounds.x, bounds.x); + } + if (posY > bounds.y || posY < -bounds.y) { + velY = -velY; + posY = p.clamp(posY, -bounds.y, bounds.y); + } + + // Write back + positions.set(posX_idx, posX); + positions.set(posY_idx, posY); + velocities.set(posX_idx, velX); + velocities.set(posY_idx, velY); + }); + }, { p, RADIUS }); + console.log(computeShader.computeSrc()); + + // Shader for rendering bouncing circles from storage buffer + bouncingCirclesShader = p.baseMaterialShader().modify(() => { + const positions = p.uniformStorage('positions'); + + p.getWorldInputs((inputs) => { + const instanceIdx = p.instanceID(); + const posX = positions.get(instanceIdx * 2); + const posY = positions.get(instanceIdx * 2 + 1); + + inputs.position.x += posX; + inputs.position.y += posY; + return inputs; + }); + }, { p }); + + // Set storage buffers for compute shader + computeShader.setUniform('positions', positionBuffer); + computeShader.setUniform('velocities', velocityBuffer); + + // Set storage buffer for rendering shader + bouncingCirclesShader.setUniform('positions', positionBuffer); }; p.draw = function () { + // Run compute shader to update physics + debugger + p.compute(computeShader, NUM_CIRCLES); + p.clear(); p.rotateY(p.millis() * 0.001); p.push(); @@ -168,6 +241,14 @@ p.model(instance, 10); p.pop(); + // Draw compute shader-driven bouncing circles + p.push(); + p.shader(bouncingCirclesShader); + p.noStroke(); + p.fill('#4ECDC4'); + p.model(circleGeometry, NUM_CIRCLES); + p.pop(); + // Test beginShape/endShape with immediate mode shapes p.push(); p.translate(0, 100, 0); diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index b9096d0f85..872db81263 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -549,3 +549,91 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) { }; return trap; } + +export function arrayAccessNode(strandsContext, bufferNode, indexNode, accessMode) { + const { dag, cfg } = strandsContext; + + // Ensure index is a StrandsNode + let index; + if (indexNode instanceof StrandsNode) { + index = indexNode; + } else { + const { id, dimension } = primitiveConstructorNode( + strandsContext, + { baseType: BaseType.INT, dimension: 1 }, + indexNode + ); + index = createStrandsNode(id, dimension, strandsContext); + } + + // Array access returns a single float + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Binary.ARRAY_ACCESS, + dependsOn: [bufferNode.id, index.id], + dimension: 1, + baseType: BaseType.FLOAT, + accessMode // 'read' or 'read_write' + }); + + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + + return { id, dimension: 1 }; +} + +export function arrayAssignmentNode(strandsContext, bufferNode, indexNode, valueNode) { + const { dag, cfg } = strandsContext; + + // Ensure index is a StrandsNode + let index; + if (indexNode instanceof StrandsNode) { + index = indexNode; + } else { + const { id, dimension } = primitiveConstructorNode( + strandsContext, + { baseType: BaseType.INT, dimension: 1 }, + indexNode + ); + index = createStrandsNode(id, dimension, strandsContext); + } + + // Ensure value is a StrandsNode + let value; + if (valueNode instanceof StrandsNode) { + value = valueNode; + } else { + const { id, dimension } = primitiveConstructorNode( + strandsContext, + { baseType: BaseType.FLOAT, dimension: 1 }, + valueNode + ); + value = createStrandsNode(id, dimension, strandsContext); + } + + // Create array access node as the assignment target + const arrayAccessData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Binary.ARRAY_ACCESS, + dependsOn: [bufferNode.id, index.id], + dimension: 1, + baseType: BaseType.FLOAT + }); + const arrayAccessID = DAG.getOrCreateNode(dag, arrayAccessData); + + // Create assignment node: buffer[index] = value + const assignmentData = DAG.createNodeData({ + nodeType: NodeType.ASSIGNMENT, + dependsOn: [arrayAccessID, value.id], + phiBlocks: [] + }); + const assignmentID = DAG.getOrCreateNode(dag, assignmentData); + + // CRITICAL: Record in CFG to preserve sequential ordering + CFG.recordInBasicBlock(cfg, cfg.currentBlock, assignmentID); + + // Track for global assignments processing + strandsContext.globalAssignments.push(assignmentID); + + return { id: assignmentID }; +} diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 91c33e7e8e..66e55a00a0 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -117,6 +117,7 @@ export const OpCode = { LOGICAL_AND: 11, LOGICAL_OR: 12, MEMBER_ACCESS: 13, + ARRAY_ACCESS: 14, }, Unary: { LOGICAL_NOT: 100, @@ -127,6 +128,7 @@ export const OpCode = { Nary: { FUNCTION_CALL: 200, CONSTRUCTOR: 201, + ARRAY_ASSIGNMENT: 202, }, ControlFlow: { RETURN: 300, diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 9eb2269a13..70fffe5a12 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -40,6 +40,7 @@ function strands(p5, fn) { ctx.uniforms = []; ctx.vertexDeclarations = new Set(); ctx.fragmentDeclarations = new Set(); + ctx.computeDeclarations = new Set(); ctx.hooks = []; ctx.globalAssignments = []; ctx.backend = backend; @@ -61,6 +62,7 @@ function strands(p5, fn) { ctx.uniforms = []; ctx.vertexDeclarations = new Set(); ctx.fragmentDeclarations = new Set(); + ctx.computeDeclarations = new Set(); ctx.hooks = []; ctx.globalAssignments = []; ctx.active = false; diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index c459374c74..859fc86b29 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -56,7 +56,7 @@ function _getBuiltinGlobalsCache(strandsContext) { function getBuiltinGlobalNode(strandsContext, name) { const spec = BUILTIN_GLOBAL_SPECS[name] if (!spec) return null - + const cache = _getBuiltinGlobalsCache(strandsContext) const uniformName = `_p5_global_${name}` const cached = cache.nodes.get(uniformName) @@ -262,6 +262,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const noiseSnippet = this._renderer.getNoiseShaderSnippet(); strandsContext.vertexDeclarations.add(noiseSnippet); strandsContext.fragmentDeclarations.add(noiseSnippet); + strandsContext.computeDeclarations.add(noiseSnippet); // Make each input into a strands node so that we can check their dimensions const strandsArgs = args.flat().map(arg => p5.strandsNode(arg)); @@ -395,6 +396,28 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } } } + + // Storage buffer uniform function for compute shaders + fn.uniformStorage = function(name, defaultValue) { + const { id, dimension } = build.variableNode( + strandsContext, + { baseType: 'storage', dimension: 1 }, + name + ); + strandsContext.uniforms.push({ + name, + typeInfo: { baseType: 'storage', dimension: 1 }, + defaultValue, + }); + + // Create StrandsNode with _originalIdentifier set (like varying variables) + // This enables proper assignment node creation and ordering preservation + const node = createStrandsNode(id, dimension, strandsContext); + node._originalIdentifier = name; + node._originalBaseType = 'storage'; + node._originalDimension = 1; + return node; + }; } ////////////////////////////////////////////// // Per-Hook functions @@ -508,10 +531,14 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const fragmentHooksWithContext = Object.fromEntries( Object.entries(shader.hooks.fragment).map(([name, hook]) => [name, { ...hook, shaderContext: 'fragment' }]) ); + const computeHooksWithContext = Object.fromEntries( + Object.entries(shader.hooks.compute).map(([name, hook]) => [name, { ...hook, shaderContext: 'compute' }]) + ); const availableHooks = { ...vertexHooksWithContext, ...fragmentHooksWithContext, + ...computeHooksWithContext, } const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); @@ -627,7 +654,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { hookType, entryBlockID, rootNodeID, - shaderContext: hookInfo?.shaderContext, // 'vertex' or 'fragment' + shaderContext: hookInfo?.shaderContext, // 'vertex', 'fragment', or 'compute' }); CFG.popBlock(cfg); }; diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 564074e245..a80aeec78b 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -7,7 +7,8 @@ export function generateShaderCode(strandsContext) { cfg, backend, vertexDeclarations, - fragmentDeclarations + fragmentDeclarations, + computeDeclarations } = strandsContext; const hooksObj = { @@ -27,6 +28,11 @@ export function generateShaderCode(strandsContext) { backend.addTextureBindingsToDeclarations(strandsContext); } + // Add storage buffer bindings to declarations for WebGPU backend + if (backend.addStorageBufferBindingsToDeclarations) { + backend.addStorageBufferBindingsToDeclarations(strandsContext); + } + for (const { hookType, rootNodeID, entryBlockID, shaderContext } of strandsContext.hooks) { const generationContext = { indent: 1, @@ -63,6 +69,8 @@ export function generateShaderCode(strandsContext) { let returnType; if (hookType.returnType.properties) { returnType = structType(hookType.returnType); + } else if (hookType.returnType.typeName === 'void') { + returnType = null; } else { if (!hookType.returnType.dataType) { throw new Error(`Missing dataType for return type ${hookType.returnType.typeName}`); @@ -95,6 +103,7 @@ export function generateShaderCode(strandsContext) { hooksObj.vertexDeclarations = [...vertexDeclarations].join('\n'); hooksObj.fragmentDeclarations = [...fragmentDeclarations].join('\n'); + hooksObj.computeDeclarations = [...computeDeclarations].join('\n'); return hooksObj; } diff --git a/src/strands/strands_node.js b/src/strands/strands_node.js index a181ff608c..da660ecfdc 100644 --- a/src/strands/strands_node.js +++ b/src/strands/strands_node.js @@ -1,4 +1,4 @@ -import { swizzleTrap, primitiveConstructorNode, variableNode } from './ir_builders'; +import { swizzleTrap, primitiveConstructorNode, variableNode, arrayAccessNode, arrayAssignmentNode } from './ir_builders'; import { BaseType, NodeType, OpCode } from './ir_types'; import { getNodeDataFromID, createNodeData, getOrCreateNode } from './ir_dag'; import { recordInBasicBlock } from './ir_cfg'; @@ -167,6 +167,42 @@ export class StrandsNode { return this; } + + get(index) { + // Validate baseType is 'storage' + const nodeData = getNodeDataFromID(this.strandsContext.dag, this.id); + if (nodeData.baseType !== 'storage') { + throw new Error('get() can only be used on storage buffers'); + } + + // Create array access node: buffer.get(index) -> buffer[index] + const { id, dimension } = arrayAccessNode( + this.strandsContext, + this, + index, + 'read' + ); + return createStrandsNode(id, dimension, this.strandsContext); + } + + set(index, value) { + // Validate baseType is 'storage' and has _originalIdentifier + const nodeData = getNodeDataFromID(this.strandsContext.dag, this.id); + if (nodeData.baseType !== 'storage') { + throw new Error('set() can only be used on storage buffers'); + } + if (!this._originalIdentifier) { + throw new Error('set() can only be used on storage buffers with an identifier'); + } + + // Create array assignment node: buffer.set(index, value) -> buffer[index] = value + // This creates an ASSIGNMENT node and records it in the CFG basic block + // CFG preserves sequential order, preventing reordering of assignments + arrayAssignmentNode(this.strandsContext, this, index, value); + + // Return this for chaining + return this; + } } export function createStrandsNode(id, dimension, strandsContext, onRebind) { return new Proxy( diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index a17bd29334..b744baff19 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1129,6 +1129,7 @@ class RendererGL extends Renderer3D { ); } + shader._compiled = true; shader._glProgram = program; shader._vertShader = vertShader; shader._fragShader = fragShader; diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index aebd0426e6..9659d0a6cd 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -10,11 +10,32 @@ const TypedArray = Object.getPrototypeOf(Uint8Array); class Shader { constructor(renderer, vertSrc, fragSrc, options = {}) { this._renderer = renderer; - this._vertSrc = vertSrc; - this._fragSrc = fragSrc; + + // Detect compute shader: first arg is STRING and second is undefined OR an options object + if ( + typeof vertSrc === 'string' && ( + fragSrc === undefined || (typeof fragSrc === 'object' && !Array.isArray(fragSrc)) + ) + ) { + // Compute shader + this.shaderType = 'compute'; + this._computeSrc = vertSrc; + this._vertSrc = null; + this._fragSrc = null; + // If fragSrc is an options object, use it + if (typeof fragSrc === 'object') { + options = fragSrc; + } + } else { + // Render shader - shaderType will be set later during binding ('fill', 'stroke', etc.) + this._vertSrc = vertSrc; + this._fragSrc = fragSrc; + this._computeSrc = null; + } + this._vertShader = -1; this._fragShader = -1; - this._glProgram = 0; + this._compiled = false; this._loadedAttributes = false; this.attributes = {}; this._loadedUniforms = false; @@ -40,6 +61,7 @@ class Shader { // Stores the hook implementations vertex: options.vertex || {}, fragment: options.fragment || {}, + compute: options.compute || {}, hookAliases: options.hookAliases || {}, @@ -48,7 +70,8 @@ class Shader { // yourShader.modify(...). modified: { vertex: (options.modified && options.modified.vertex) || {}, - fragment: (options.modified && options.modified.fragment) || {} + fragment: (options.modified && options.modified.fragment) || {}, + compute: (options.modified && options.modified.compute) || {}, } }; } @@ -80,13 +103,20 @@ class Shader { } vertSrc() { + if (this.shaderType === 'compute') return null; return this.shaderSrc(this._vertSrc, 'vertex'); } fragSrc() { + if (this.shaderType === 'compute') return null; return this.shaderSrc(this._fragSrc, 'fragment'); } + computeSrc() { + if (this.shaderType !== 'compute') return null; + return this.shaderSrc(this._computeSrc, 'compute'); + } + /** * Logs the hooks available in this shader, and their current implementation. * @@ -143,22 +173,33 @@ class Shader { * @beta */ inspectHooks() { - console.log('==== Vertex shader hooks: ===='); - for (const key in this.hooks.vertex) { - console.log( - (this.hooks.modified.vertex[key] ? '[MODIFIED] ' : '') + - key + - this.hooks.vertex[key] - ); - } - console.log(''); - console.log('==== Fragment shader hooks: ===='); - for (const key in this.hooks.fragment) { - console.log( - (this.hooks.modified.fragment[key] ? '[MODIFIED] ' : '') + - key + - this.hooks.fragment[key] - ); + if (this.shaderType === 'compute') { + console.log('==== Compute shader hooks: ===='); + for (const key in this.hooks.compute) { + console.log( + (this.hooks.modified.compute[key] ? '[MODIFIED] ' : '') + + key + + this.hooks.compute[key] + ); + } + } else { + console.log('==== Vertex shader hooks: ===='); + for (const key in this.hooks.vertex) { + console.log( + (this.hooks.modified.vertex[key] ? '[MODIFIED] ' : '') + + key + + this.hooks.vertex[key] + ); + } + console.log(''); + console.log('==== Fragment shader hooks: ===='); + for (const key in this.hooks.fragment) { + console.log( + (this.hooks.modified.fragment[key] ? '[MODIFIED] ' : '') + + key + + this.hooks.fragment[key] + ); + } } console.log(''); console.log('==== Helper functions: ===='); @@ -362,6 +403,7 @@ class Shader { const newHooks = { vertex: {}, fragment: {}, + compute: {}, helpers: {} }; for (const key in hooks) { @@ -374,16 +416,22 @@ class Shader { } else if (key === 'fragmentDeclarations') { newHooks.fragment.declarations = (newHooks.fragment.declarations || '') + '\n' + hooks[key]; + } else if (key === 'computeDeclarations') { + newHooks.compute.declarations = + (newHooks.compute.declarations || '') + '\n' + hooks[key]; } else if (this.hooks.vertex[key]) { newHooks.vertex[key] = hooks[key]; } else if (this.hooks.fragment[key]) { newHooks.fragment[key] = hooks[key]; + } else if (this.hooks.compute[key]) { + newHooks.compute[key] = hooks[key]; } else { newHooks.helpers[key] = hooks[key]; } } const modifiedVertex = Object.assign({}, this.hooks.modified.vertex); const modifiedFragment = Object.assign({}, this.hooks.modified.fragment); + const modifiedCompute = Object.assign({}, this.hooks.modified.compute); for (const key in newHooks.vertex || {}) { if (key === 'declarations') continue; modifiedVertex[key] = true; @@ -392,21 +440,35 @@ class Shader { if (key === 'declarations') continue; modifiedFragment[key] = true; } + for (const key in newHooks.compute || {}) { + if (key === 'declarations') continue; + modifiedCompute[key] = true; + } - return new Shader(this._renderer, this._vertSrc, this._fragSrc, { + const args = [this._renderer]; + if (this.shaderType === 'compute') { + args.push(this._computeSrc); + } else { + args.push(this._vertSrc, this._fragSrc); + } + args.push({ declarations: (this.hooks.declarations || '') + '\n' + (hooks.declarations || ''), uniforms: Object.assign({}, this.hooks.uniforms, hooks.uniforms || {}), varyingVariables: (hooks.varyingVariables || []).concat(this.hooks.varyingVariables || []), fragment: Object.assign({}, this.hooks.fragment, newHooks.fragment || {}), vertex: Object.assign({}, this.hooks.vertex, newHooks.vertex || {}), + compute: Object.assign({}, this.hooks.compute, newHooks.compute || {}), helpers: Object.assign({}, this.hooks.helpers, newHooks.helpers || {}), hookAliases: Object.assign({}, this.hooks.hookAliases, newHooks.hookAliases || {}), modified: { vertex: modifiedVertex, - fragment: modifiedFragment + fragment: modifiedFragment, + compute: modifiedCompute, } }); + + return new Shader(...args); } /** @@ -428,7 +490,9 @@ class Shader { ); } - this._loadAttributes(); + if (this.shaderType !== 'compute') { + this._loadAttributes(); + } this._loadUniforms(); this._renderer._finalizeShader(this); @@ -631,11 +695,14 @@ class Shader { * } */ copyToContext(context) { - const shader = new Shader( - context._renderer, - this._vertSrc, - this._fragSrc - ); + const args = [context._renderer]; + if (this.shaderType === 'compute') { + args.push(this._computeSrc); + } else { + args.push(this._vertSrc, this._fragSrc); + } + args.push(this.hooks); + const shader = new Shader(...args); shader.ensureCompiledOnContext(context._renderer); return shader; } @@ -644,11 +711,11 @@ class Shader { * @private */ ensureCompiledOnContext(context) { - if (this._glProgram !== 0 && this._renderer !== context) { + if (this._compiled && this._renderer !== context) { throw new Error( 'The shader being run is attached to a different context. Do you need to copy it to this context first with .copyToContext()?' ); - } else if (this._glProgram === 0) { + } else if (!this._compiled) { this._renderer = context?._renderer?.filterRenderer?._renderer || context; this.init(); } diff --git a/src/webgl/strands_glslBackend.js b/src/webgl/strands_glslBackend.js index daf804a8e8..f976b2347a 100644 --- a/src/webgl/strands_glslBackend.js +++ b/src/webgl/strands_glslBackend.js @@ -231,6 +231,10 @@ export const glslBackend = { return `${typeName} ${tmp} = ${expr};`; }, generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType) { + if (!returnType) { + generationContext.write('return;'); + return; + } const dag = strandsContext.dag; const rootNode = getNodeDataFromID(dag, rootNodeID); if (isStructType(returnType)) { diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 42946ebf34..70de269a2a 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -11,6 +11,7 @@ import { wgslBackend } from './strands_wgslBackend'; import noiseWGSL from './shaders/functions/noise3DWGSL'; import { baseFilterVertexShader, baseFilterFragmentShader } from './shaders/filters/base'; import { imageLightVertexShader, imageLightDiffusedFragmentShader, imageLightSpecularFragmentShader } from './shaders/imageLight'; +import { baseComputeShader } from './shaders/compute'; const FRAME_STATE = { PENDING: 0, @@ -84,6 +85,9 @@ function rendererWebGPU(p5, fn) { // Retired buffers to destroy at end of frame this._retiredBuffers = []; + // Storage buffers for compute shaders + this._storageBuffers = new Set(); + // 2D canvas for pixel reading fallback this._pixelReadCanvas = null; this._pixelReadCtx = null; @@ -493,7 +497,8 @@ function rendererWebGPU(p5, fn) { return 4; // Cap at 4 for broader compatibility } - _shaderOptions({ mode }) { + _shaderOptions({ mode, compute, workgroupSize }) { + if (compute) return { compute: true, workgroupSize }; const activeFramebuffer = this.activeFramebuffer(); const format = activeFramebuffer ? this._getWebGPUColorFormat(activeFramebuffer) : @@ -534,6 +539,31 @@ function rendererWebGPU(p5, fn) { _initShader(shader) { const device = this.device; + if (shader.shaderType === 'compute') { + // Compute shader initialization + shader.computeModule = device.createShaderModule({ code: shader.computeSrc() }); + shader._computePipelineCache = null; + shader._workgroupSize = null; + + // Create compute pipeline (deferred until first compute() call) + shader.getPipeline = ({ workgroupSize }) => { + if (!shader._computePipelineCache) { + shader._computePipelineCache = device.createComputePipeline({ + layout: shader._pipelineLayout, + compute: { + module: shader.computeModule, + entryPoint: 'main' + } + }); + shader._workgroupSize = workgroupSize; + } + return shader._computePipelineCache; + }; + + return; + } + + // Render shader initialization shader.vertModule = device.createShaderModule({ code: shader.vertSrc() }); shader.fragModule = device.createShaderModule({ code: shader.fragSrc() }); @@ -632,7 +662,9 @@ function rendererWebGPU(p5, fn) { entries.push({ bufferGroup, binding: bufferGroup.binding, - visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + visibility: shader.shaderType === 'compute' + ? GPUShaderStage.COMPUTE + : GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform', hasDynamicOffset: bufferGroup.dynamic }, }); structEntries.set(bufferGroup.group, entries); @@ -666,6 +698,24 @@ function rendererWebGPU(p5, fn) { groupEntries.set(group, entries); } + // Add storage buffer bindings + for (const storage of shader._storageBuffers || []) { + const group = storage.group; + const entries = groupEntries.get(group) || []; + + entries.push({ + binding: storage.binding, + visibility: storage.visibility, + buffer: { + type: storage.accessMode === 'read' ? 'read-only-storage' : 'storage' + }, + storage: storage, + }); + + entries.sort((a, b) => a.binding - b.binding); + groupEntries.set(group, entries); + } + // Create layouts and bind groups const groupEntriesArr = []; for (const [group, entries] of groupEntries) { @@ -684,6 +734,7 @@ function rendererWebGPU(p5, fn) { shader._pipelineLayout = this.device.createPipelineLayout({ bindGroupLayouts: shader._bindGroupLayouts, }); + shader._compiled = true; } _getBlendState(mode) { @@ -1442,25 +1493,9 @@ function rendererWebGPU(p5, fn) { this._beginActiveRenderPass(); const passEncoder = this.activeRenderPass; - const currentShader = this._curShader; - const shaderOptions = this._shaderOptions({ mode }); - if (this.activeShader !== currentShader || this._shaderOptionsDifferent(shaderOptions)) { - passEncoder.setPipeline(currentShader.getPipeline(shaderOptions)); - } - this.activeShader = currentShader; - this.activeShaderOptions = shaderOptions; - // Set stencil reference value for clipping - const drawTarget = this.drawTarget(); - if (drawTarget._isClipApplied && !this._clipping) { - // When using the clip mask, test against reference value 0 (background) - // WebGL uses NOTEQUAL with ref 0, so fragments pass where stencil != 0 - // In WebGPU with 'not-equal', we need ref 0 to pass where stencil != 0 - passEncoder.setStencilReference(0); - } else if (this._clipping) { - // When writing to the clip mask, write reference value 1 - passEncoder.setStencilReference(1); - } + const currentShader = this._curShader; + this.setupShaderBindGroups(currentShader, passEncoder, { mode, buffers }); // Bind vertex buffers for (const buffer of currentShader._vertexBuffers || this._getVertexBuffers(currentShader)) { const location = currentShader.attributes[buffer.attr].location; @@ -1468,6 +1503,58 @@ function rendererWebGPU(p5, fn) { passEncoder.setVertexBuffer(location, gpuBuffer, 0); } + if (currentShader.shaderType === "fill") { + // Bind index buffer and issue draw + if (buffers.indexBuffer) { + const indexFormat = buffers.indexFormat || "uint16"; + passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat); + passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0); + } else { + passEncoder.draw(geometry.vertices.length, count, 0, 0); + } + } else if (currentShader.shaderType === "text") { + if (!buffers.indexBuffer) { + throw new Error("Text geometry must have an index buffer"); + } + const indexFormat = buffers.indexFormat || "uint16"; + passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat); + passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0); + } + + if (buffers.lineVerticesBuffer && currentShader.shaderType === "stroke") { + passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0); + } + + // Mark that we have pending draws that need submission + this._hasPendingDraws = true; + } + + setupShaderBindGroups(currentShader, passEncoder, shaderOptionsParams) { + const shaderOptions = this._shaderOptions(shaderOptionsParams); + if ( + shaderOptions.compute || + this.activeShader !== currentShader || + this._shaderOptionsDifferent(shaderOptions) + ) { + passEncoder.setPipeline(currentShader.getPipeline(shaderOptions)); + } + if (!shaderOptions.compute) { + this.activeShader = currentShader; + this.activeShaderOptions = shaderOptions; + + // Set stencil reference value for clipping + const drawTarget = this.drawTarget(); + if (drawTarget._isClipApplied && !this._clipping) { + // When using the clip mask, test against reference value 0 (background) + // WebGL uses NOTEQUAL with ref 0, so fragments pass where stencil != 0 + // In WebGPU with 'not-equal', we need ref 0 to pass where stencil != 0 + passEncoder.setStencilReference(0); + } else if (this._clipping) { + // When writing to the clip mask, write reference value 1 + passEncoder.setStencilReference(1); + } + } + for (const bufferGroup of currentShader._uniformBufferGroups) { if (bufferGroup.dynamic) { // Bind uniforms into a part of a big dynamic memory block because @@ -1549,6 +1636,19 @@ function rendererWebGPU(p5, fn) { : { buffer: uniformBufferInfo.buffer }, }); } + } else if (entry.storage && !bindGroup) { + // Storage buffer binding + const uniform = currentShader.uniforms[entry.storage.name]; + if (!uniform || !uniform._cachedData || !uniform._cachedData._isStorageBuffer) { + throw new Error( + `Storage buffer "${entry.storage.name}" not set. ` + + `Use shader.setUniform("${entry.storage.name}", storageBuffer)` + ); + } + bgEntries.push({ + binding: entry.binding, + resource: { buffer: uniform._cachedData.buffer }, + }); } else if (!bindGroup) { bgEntries.push({ binding: entry.binding, @@ -1582,31 +1682,7 @@ function rendererWebGPU(p5, fn) { ); } } - - if (currentShader.shaderType === "fill") { - // Bind index buffer and issue draw - if (buffers.indexBuffer) { - const indexFormat = buffers.indexFormat || "uint16"; - passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat); - passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0); - } else { - passEncoder.draw(geometry.vertices.length, count, 0, 0); - } - } else if (currentShader.shaderType === "text") { - if (!buffers.indexBuffer) { - throw new Error("Text geometry must have an index buffer"); - } - const indexFormat = buffers.indexFormat || "uint16"; - passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat); - passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0); - } - - if (buffers.lineVerticesBuffer && currentShader.shaderType === "stroke") { - passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0); - } - - // Mark that we have pending draws that need submission - this._hasPendingDraws = true; + return passEncoder; } ////////////////////////////////////////////// @@ -1806,10 +1882,11 @@ function rendererWebGPU(p5, fn) { const uniformVarRegex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var\s+(\w+)\s*:\s*(\w+);/g; let match; - while ((match = uniformVarRegex.exec(shader.vertSrc())) !== null) { + const src = shader.shaderType === 'compute' ? shader.computeSrc() : shader.vertSrc(); + while ((match = uniformVarRegex.exec(src)) !== null) { const [_, groupNum, binding, varName, structType] = match; const bindingIndex = parseInt(binding); - const uniforms = this._parseStruct(shader.vertSrc(), structType); + const uniforms = this._parseStruct(src, structType); uniformGroups.push({ group: parseInt(groupNum), @@ -1820,7 +1897,7 @@ function rendererWebGPU(p5, fn) { }); } - if (uniformGroups.length === 0) { + if (uniformGroups.length === 0 && shader.shaderType !== 'compute') { throw new Error('Expected at least one uniform struct bound to @group(0)'); } @@ -1847,6 +1924,10 @@ function rendererWebGPU(p5, fn) { // TODO: support other texture types const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d|sampler);/g; + // Extract storage buffers + const storageBuffers = {}; + const storageRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*array/g; + // Track which bindings are taken by the struct properties we've parsed // (the rest should be textures/samplers) const structUniformBindings = {}; @@ -1856,8 +1937,11 @@ function rendererWebGPU(p5, fn) { for (const [src, visibility] of [ [shader.vertSrc(), GPUShaderStage.VERTEX], - [shader.fragSrc(), GPUShaderStage.FRAGMENT] + [shader.fragSrc(), GPUShaderStage.FRAGMENT], + [shader.computeSrc ? shader.computeSrc() : null, GPUShaderStage.COMPUTE] ]) { + if (!src) continue; // Skip if shader stage doesn't exist + let match; while ((match = samplerRegex.exec(src)) !== null) { const [_, group, binding, name, type] = match; @@ -1892,21 +1976,51 @@ function rendererWebGPU(p5, fn) { samplerNode.textureSource = sampler; } } + + // Parse storage buffers + while ((match = storageRegex.exec(src)) !== null) { + const [_, group, binding, accessMode, name] = match; + const groupIndex = parseInt(group); + const bindingIndex = parseInt(binding); + + const key = `${groupIndex},${bindingIndex}`; + const existing = storageBuffers[key]; + // If any stage uses read_write, the bind group layout must use read_write + const finalAccessMode = (existing?.accessMode === 'read_write' || accessMode === 'read_write') + ? 'read_write' + : accessMode; + + storageBuffers[key] = { + visibility: (existing?.visibility || 0) | visibility, + group: groupIndex, + binding: bindingIndex, + name, + accessMode: finalAccessMode, // 'read' or 'read_write' + isStorage: true, + type: 'storage' + }; + } } - return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers)]; + + // Store storage buffers on shader for later use + shader._storageBuffers = Object.values(storageBuffers); + + return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers), ...Object.values(storageBuffers)]; } - getNextBindingIndex({ vert, frag }, group = 0) { + getNextBindingIndex({ vert, frag, compute }, group = 0) { // Get the highest binding index in the specified group and return the next available - const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d|sampler|uniform)/g; + const bindingRegex = /@group\((\d+)\)\s*@binding\((\d+)\)/g; let maxBindingIndex = -1; - for (const [src, visibility] of [ - [vert, GPUShaderStage.VERTEX], - [frag, GPUShaderStage.FRAGMENT] - ]) { + const sources = []; + if (vert) sources.push([vert, GPUShaderStage.VERTEX]); + if (frag) sources.push([frag, GPUShaderStage.FRAGMENT]); + if (compute) sources.push([compute, GPUShaderStage.COMPUTE]); + + for (const [src, visibility] of sources) { let match; - while ((match = samplerRegex.exec(src)) !== null) { + while ((match = bindingRegex.exec(src)) !== null) { const [_, groupIndex, bindingIndex] = match; if (parseInt(groupIndex) === group) { maxBindingIndex = Math.max(maxBindingIndex, parseInt(bindingIndex)); @@ -2238,8 +2352,8 @@ function rendererWebGPU(p5, fn) { } ); - let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment)\s*)?fn main[^{]+\{)/); - if (shaderType !== 'fragment') { + let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment|compute)\s*(?:@workgroup_size\([^)]+\)\s*)?)?fn main[^{]+\{)/); + if (shaderType === 'vertex') { if (!main.match(/\@builtin\s*\(\s*instance_index\s*\)/)) { main = main.replace(/\)\s*(->|\{)/, ', @builtin(instance_index) instanceID: u32) $1'); } @@ -2259,6 +2373,7 @@ function rendererWebGPU(p5, fn) { const nextBinding = this.getNextBindingIndex({ vert: shaderType === 'vertex' ? preMain + (shader.hooks.vertex?.declarations ?? '') + shader.hooks.declarations : shader._vertSrc, frag: shaderType === 'fragment' ? preMain + (shader.hooks.fragment?.declarations ?? '') + shader.hooks.declarations : shader._fragSrc, + compute: shaderType === 'compute' ? preMain + (shader.hooks.compute?.declarations ?? '') + shader.hooks.declarations : shader._computeSrc, }, 0); // Create HookUniforms struct and binding @@ -2269,8 +2384,14 @@ ${hookUniformFields}} @group(0) @binding(${nextBinding}) var hooks: HookUniforms; `; - // Insert before the first @group binding - preMain = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`); + // Insert before the first @group binding, or at the end if there are none + const replaced = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`); + if (replaced === preMain) { + // No @group bindings found in base shader, append to preMain + preMain = preMain + '\n' + hookUniformsDecl; + } else { + preMain = replaced; + } } // Handle varying variables by injecting them into VertexOutput and FragmentInput structs @@ -2352,7 +2473,7 @@ ${hookUniformFields}} if (shader.hooks.declarations) { hooks += shader.hooks.declarations + '\n'; } - if (shader.hooks[shaderType].declarations) { + if (shader.hooks[shaderType] && shader.hooks[shaderType].declarations) { hooks += shader.hooks[shaderType].declarations + '\n'; } for (const hookDef in shader.hooks.helpers) { @@ -2376,7 +2497,7 @@ ${hookUniformFields}} let [_, params, body] = /^(\([^\)]*\))((?:.|\n)*)$/.exec(shader.hooks[shaderType][hookDef]); - if (shaderType !== 'fragment') { + if (shaderType === 'vertex') { // Splice the instance ID in as a final parameter to every WGSL hook function let hasParams = !!params.match(/^\(\s*\S+.*\)$/); params = params.slice(0, -1) + (hasParams ? ', ' : '') + 'instanceID: u32)'; @@ -2390,7 +2511,7 @@ ${hookUniformFields}} } // Add the instance ID as a final parameter to each hook call - if (shaderType !== 'fragment') { + if (shaderType === 'vertex') { const addInstanceIDParam = (src) => { let result = src; let idx = 0; @@ -2481,6 +2602,10 @@ ${hookUniformFields}} body = shader.hooks.fragment[hookName]; fullSrc = shader._fragSrc; } + if (!body) { + body = shader.hooks.compute[hookName]; + fullSrc = shader._computeSrc; + } if (!body) { throw new Error(`Can't find hook ${hookName}!`); } @@ -2805,6 +2930,59 @@ ${hookUniformFields}} }; } + createStorage(dataOrCount) { + const device = this.device; + + // Determine buffer size and initial data + let size, initialData; + if (typeof dataOrCount === 'number') { + // createStorage(count) - zero-initialized + size = dataOrCount * 4; // floats are 4 bytes + initialData = new Float32Array(dataOrCount); + } else { + // createStorage(array) - from data + if (dataOrCount instanceof Float32Array) { + initialData = dataOrCount; + } else if (Array.isArray(dataOrCount)) { + initialData = new Float32Array(dataOrCount); + } else { + throw new Error('createStorage expects a number or array/Float32Array'); + } + size = initialData.byteLength; + } + + // Align to 16 bytes (WGSL storage buffer alignment requirement) + size = Math.ceil(size / 16) * 16; + + // Create storage buffer with STORAGE | COPY_DST | COPY_SRC usage + const buffer = device.createBuffer({ + size, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, + mappedAtCreation: initialData.length > 0 + }); + + // Write initial data if provided + if (initialData.length > 0) { + const mapping = new Float32Array(buffer.getMappedRange()); + mapping.set(initialData); + buffer.unmap(); + } + + // Return wrapper object with metadata + const storageBuffer = { + _isStorageBuffer: true, + buffer, + size, + elementCount: size / 4, // Number of floats + _renderer: this + }; + + // Track for cleanup + this._storageBuffers.add(storageBuffer); + + return storageBuffer; + } + _getWebGPUColorFormat(framebuffer) { if (framebuffer.format === constants.FLOAT) { return framebuffer.channels === RGBA ? 'rgba32float' : 'rgba32float'; @@ -3128,6 +3306,21 @@ ${hookUniformFields}} return this._baseFilterShader; } + baseComputeShader() { + if (!this._baseComputeShader) { + this._baseComputeShader = new Shader( + this, + baseComputeShader, + { + compute: { + 'void processData': '(inputs: ComputeInputs) {}', + }, + } + ); + } + return this._baseComputeShader; + } + /* * WebGPU-specific implementation of imageLight shader creation */ @@ -3227,6 +3420,46 @@ ${hookUniformFields}} glDataType: dataType || 'uint8' }; } + + compute(shader, x, y = 1, z = 1) { + if (shader.shaderType !== 'compute') { + throw new Error('compute() can only be called with a compute shader'); + } + + this._finishActiveRenderPass(); + + // Ensure shader is initialized and finalized + if (!shader._compiled) { + shader.init(); + } + + // Set default uniforms + shader.setDefaultUniforms(); + shader.setUniform('uTotalCount', [x, y, z]); + + // Calculate optimal workgroup size (8x8x1 = 64 threads per workgroup) + const WORKGROUP_SIZE_X = 8; + const WORKGROUP_SIZE_Y = 8; + const WORKGROUP_SIZE_Z = 1; + + // Calculate number of workgroups needed + const workgroupCountX = Math.ceil(x / WORKGROUP_SIZE_X); + const workgroupCountY = Math.ceil(y / WORKGROUP_SIZE_Y); + const workgroupCountZ = Math.ceil(z / WORKGROUP_SIZE_Z); + + const commandEncoder = this.device.createCommandEncoder(); + const passEncoder = commandEncoder.beginComputePass(); + this.setupShaderBindGroups(shader, passEncoder, { + compute: true, + workgroupSize: [WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y, WORKGROUP_SIZE_Z], + }); + + // Dispatch compute workgroups + passEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY, workgroupCountZ); + + passEncoder.end(); + this.device.queue.submit([commandEncoder.finish()]); + } } p5.RendererWebGPU = RendererWebGPU; @@ -3237,6 +3470,61 @@ ${hookUniformFields}} fn.setAttributes = async function (key, value) { return this._renderer._setAttributes(key, value); } + + /** + * Creates a storage buffer for use in compute shaders. + * + * @method createStorage + * @param {Number|Array|Float32Array} dataOrCount Either a number specifying the count of floats, + * or an array/Float32Array with initial data. + * @returns {Object} A storage buffer object. + */ + fn.createStorage = function (dataOrCount) { + return this._renderer.createStorage(dataOrCount); + } + + /** + * Returns the base compute shader. + * + * Calling `buildComputeShader(shaderFunction)` is equivalent to + * calling `baseComputeShader().modify(shaderFunction)`. + * + * @method baseComputeShader + * @submodule p5.strands + * @beta + * @returns {p5.Shader} The base compute shader. + */ + fn.baseComputeShader = function () { + return this._renderer.baseComputeShader(); + }; + + /** + * Create a new compute shader using p5.strands. + * + * @method buildComputeShader + * @submodule p5.strands + * @beta + * @param {Function} callback A function building a p5.strands compute shader. + * @returns {p5.Shader} The compute shader. + */ + fn.buildComputeShader = function (cb, context) { + return this.baseComputeShader().modify(cb, context); + }; + + /** + * Dispatches a compute shader to run on the GPU. + * + * @method compute + * @submodule p5.strands + * @beta + * @param {p5.Shader} shader The compute shader to run. + * @param {Number} x Number of invocations in the X dimension. + * @param {Number} [y=1] Number of invocations in the Y dimension. + * @param {Number} [z=1] Number of invocations in the Z dimension. + */ + fn.compute = function (shader, x, y, z) { + this._renderer.compute(shader, x, y, z); + }; } export default rendererWebGPU; diff --git a/src/webgpu/shaders/compute.js b/src/webgpu/shaders/compute.js new file mode 100644 index 0000000000..11d86fcbfc --- /dev/null +++ b/src/webgpu/shaders/compute.js @@ -0,0 +1,42 @@ +export const baseComputeShader = ` +struct ComputeInputs { + globalId: vec3, + localId: vec3, + workgroupId: vec3, + localIndex: i32, +} + +struct ComputeUniforms { + uTotalCount: vec3, +} +@group(0) @binding(0) var uniforms: ComputeUniforms; + +fn processData(inputs: ComputeInputs) { + HOOK_processData(inputs); +} + +@compute @workgroup_size(8, 8, 1) +fn main( + @builtin(global_invocation_id) globalId: vec3, + @builtin(local_invocation_id) localId: vec3, + @builtin(workgroup_id) workgroupId: vec3, + @builtin(local_invocation_index) localIndex: u32 +) { + var inputs: ComputeInputs; + inputs.globalId = vec3(globalId); + + if ( + inputs.globalId.x > uniforms.uTotalCount.x || + inputs.globalId.y > uniforms.uTotalCount.y || + inputs.globalId.z > uniforms.uTotalCount.z + ) { + return; + } + + inputs.localId = vec3(localId); + inputs.workgroupId = vec3(workgroupId); + inputs.localIndex = i32(localIndex); + + processData(inputs); +} +`; diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index 22ae1d62d7..aef656928a 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -195,10 +195,10 @@ export const wgslBackend = { // Add texture and sampler bindings for sampler2D uniforms to both vertex and fragment declarations if (!strandsContext.renderer || !strandsContext.baseShader) return; - // Get the next available binding index from the renderer let bindingIndex = strandsContext.renderer.getNextBindingIndex({ - vert: strandsContext.baseShader.vertSrc(), - frag: strandsContext.baseShader.fragSrc(), + vert: strandsContext.baseShader._vertSrc, + frag: strandsContext.baseShader._fragSrc, + compute: strandsContext.baseShader._computeSrc, }); for (const {name, typeInfo} of strandsContext.uniforms) { @@ -215,6 +215,31 @@ export const wgslBackend = { } } }, + addStorageBufferBindingsToDeclarations(strandsContext) { + if (!strandsContext.renderer || !strandsContext.baseShader) return; + + const isComputeShader = strandsContext.baseShader.shaderType === 'compute'; + let bindingIndex = strandsContext.renderer.getNextBindingIndex({ + vert: strandsContext.baseShader._vertSrc, + frag: strandsContext.baseShader._fragSrc, + compute: strandsContext.baseShader._computeSrc, + }); + + for (const {name, typeInfo} of strandsContext.uniforms) { + if (typeInfo.baseType === 'storage') { + if (isComputeShader) { + const storageBinding = `@group(0) @binding(${bindingIndex}) var ${name}: array;`; + strandsContext.computeDeclarations.add(storageBinding); + } else { + const storageBinding = `@group(0) @binding(${bindingIndex}) var ${name}: array;`; + strandsContext.vertexDeclarations.add(storageBinding); + strandsContext.fragmentDeclarations.add(storageBinding); + } + + bindingIndex += 1; + } + } + }, getTypeName(baseType, dimension) { const primitiveTypeName = TypeNames[baseType + dimension] if (!primitiveTypeName) { @@ -228,6 +253,11 @@ export const wgslBackend = { if (typeInfo.baseType === 'sampler2D') { return null; // Signal that this should not be added to uniform struct } + // For storage buffers, we don't add them to the uniform struct + // Instead, they become separate storage buffer bindings + if (typeInfo.baseType === 'storage') { + return null; // Signal that this should not be added to uniform struct + } return `${name}: ${this.getTypeName(typeInfo.baseType, typeInfo.dimension)}`; }, generateVaryingVariable(varName, typeInfo) { @@ -268,6 +298,16 @@ export const wgslBackend = { const targetNode = getNodeDataFromID(dag, targetNodeID); const semicolon = generationContext.suppressSemicolon ? '' : ';'; + // Check if target is an array access (storage buffer assignment) + if (targetNode.opCode === OpCode.Binary.ARRAY_ACCESS) { + const [bufferID, indexID] = targetNode.dependsOn; + const bufferExpr = this.generateExpression(generationContext, dag, bufferID); + const indexExpr = this.generateExpression(generationContext, dag, indexID); + const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID); + generationContext.write(`${bufferExpr}[i32(${indexExpr})] = ${sourceExpr}${semicolon}`); + return; + } + // Check if target is a swizzle assignment if (targetNode.opCode === OpCode.Unary.SWIZZLE) { const parentID = targetNode.dependsOn[0]; @@ -325,6 +365,10 @@ export const wgslBackend = { return `var ${tmp}: ${typeName} = ${expr};`; }, generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType) { + if (!returnType) { + generationContext.write('return;'); + return; + } const dag = strandsContext.dag; const rootNode = getNodeDataFromID(dag, rootNodeID); if (isStructType(returnType)) { @@ -365,9 +409,9 @@ export const wgslBackend = { } } - // Check if this is a uniform variable (but not a texture) + // Check if this is a uniform variable (but not a texture or storage buffer) const uniform = generationContext.strandsContext?.uniforms?.find(uniform => uniform.name === node.identifier); - if (uniform && uniform.typeInfo.baseType !== 'sampler2D') { + if (uniform && uniform.typeInfo.baseType !== 'sampler2D' && uniform.typeInfo.baseType !== 'storage') { return `hooks.${node.identifier}`; } @@ -420,6 +464,12 @@ export const wgslBackend = { const parentExpr = this.generateExpression(generationContext, dag, parentID); return `${parentExpr}.${node.swizzle}`; } + if (node.opCode === OpCode.Binary.ARRAY_ACCESS) { + const [bufferID, indexID] = node.dependsOn; + const bufferExpr = this.generateExpression(generationContext, dag, bufferID); + const indexExpr = this.generateExpression(generationContext, dag, indexID); + return `${bufferExpr}[i32(${indexExpr})]`; + } if (node.dependsOn.length === 2) { const [lID, rID] = node.dependsOn; const left = this.generateExpression(generationContext, dag, lID); From 93096c8b257a5e2958062cadc8151e1e3be4e076 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 15 Feb 2026 11:19:53 -0500 Subject: [PATCH 031/250] Auto-begin/end the default hook for compute shaders --- preview/index.html | 63 ++++++++++++++++----------------- src/strands/p5.strands.js | 7 ++-- src/webgpu/p5.RendererWebGPU.js | 4 +-- src/webgpu/shaders/compute.js | 18 ++++------ 4 files changed, 44 insertions(+), 48 deletions(-) diff --git a/preview/index.html b/preview/index.html index c4df32b6bd..d9c4471344 100644 --- a/preview/index.html +++ b/preview/index.html @@ -115,41 +115,38 @@ const bounds = p.uniformVec2(() => [p.width / 2 - RADIUS, p.height / 2 - RADIUS]); const deltaTime = p.uniformFloat(() => p.deltaTime * 0.1); - p.processData((inputs) => { - const idx = inputs.globalId.x; - - // Each circle has 2 floats (x, y) - const posX_idx = idx * 2; - const posY_idx = idx * 2 + 1; - - // Read current position and velocity - let posX = positions.get(posX_idx); - let posY = positions.get(posY_idx); - let velX = velocities.get(posX_idx); - let velY = velocities.get(posY_idx); - - // Update position - posX = posX + velX * deltaTime; - posY = posY + velY * deltaTime; - - // Bounce off boundaries (DVD logo style) - if (posX > bounds.x || posX < -bounds.x) { - velX = -velX; - posX = p.clamp(posX, -bounds.x, bounds.x); - } - if (posY > bounds.y || posY < -bounds.y) { - velY = -velY; - posY = p.clamp(posY, -bounds.y, bounds.y); - } + const idx = iteration.index.x; + + // Each circle has 2 floats (x, y) + const posX_idx = idx * 2; + const posY_idx = idx * 2 + 1; + + // Read current position and velocity + let posX = positions.get(posX_idx); + let posY = positions.get(posY_idx); + let velX = velocities.get(posX_idx); + let velY = velocities.get(posY_idx); + + // Update position + posX = posX + velX * deltaTime; + posY = posY + velY * deltaTime; + + // Bounce off boundaries (DVD logo style) + if (posX > bounds.x || posX < -bounds.x) { + velX = -velX; + posX = p.clamp(posX, -bounds.x, bounds.x); + } + if (posY > bounds.y || posY < -bounds.y) { + velY = -velY; + posY = p.clamp(posY, -bounds.y, bounds.y); + } - // Write back - positions.set(posX_idx, posX); - positions.set(posY_idx, posY); - velocities.set(posX_idx, velX); - velocities.set(posY_idx, velY); - }); + // Write back + positions.set(posX_idx, posX); + positions.set(posY_idx, posY); + velocities.set(posX_idx, velX); + velocities.set(posY_idx, velY); }, { p, RADIUS }); - console.log(computeShader.computeSrc()); // Shader for rendering bouncing circles from storage buffer bouncingCirclesShader = p.baseMaterialShader().modify(() => { diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 70fffe5a12..00deddb5a6 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -105,7 +105,7 @@ function strands(p5, fn) { ////////////////////////////////////////////// const oldModify = p5.Shader.prototype.modify; - p5.Shader.prototype.modify = function (shaderModifier, scope = {}) { + p5.Shader.prototype.modify = function (shaderModifier, scope = {}, options = {}) { try { if ( shaderModifier instanceof Function || @@ -120,7 +120,8 @@ function strands(p5, fn) { }); createShaderHooksFunctions(strandsContext, fn, this); // TODO: expose this, is internal for debugging for now. - const options = { parser: true, srcLocations: false }; + options.parser = true; + options.srcLocations = false; // 1. Transpile from strands DSL to JS let strandsCallback; @@ -147,11 +148,13 @@ function strands(p5, fn) { BlockType.GLOBAL, ); pushBlock(strandsContext.cfg, globalScope); + if (options.hook) strandsContext.renderer._pInst[options.hook].begin(); if (strandsContext.renderer?._pInst?._runStrandsInGlobalMode) { withTempGlobalMode(strandsContext.renderer._pInst, strandsCallback); } else { strandsCallback(); } + if (options.hook) strandsContext.renderer._pInst[options.hook].end(); popBlock(strandsContext.cfg); // 3. Generate shader code hooks object from the IR diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 70de269a2a..841dabdbb3 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -3313,7 +3313,7 @@ ${hookUniformFields}} baseComputeShader, { compute: { - 'void processData': '(inputs: ComputeInputs) {}', + 'void iteration': '(inputs: ComputeInputs) {}', }, } ); @@ -3508,7 +3508,7 @@ ${hookUniformFields}} * @returns {p5.Shader} The compute shader. */ fn.buildComputeShader = function (cb, context) { - return this.baseComputeShader().modify(cb, context); + return this.baseComputeShader().modify(cb, context, { hook: 'iteration' }); }; /** diff --git a/src/webgpu/shaders/compute.js b/src/webgpu/shaders/compute.js index 11d86fcbfc..0c2962b43d 100644 --- a/src/webgpu/shaders/compute.js +++ b/src/webgpu/shaders/compute.js @@ -1,9 +1,9 @@ export const baseComputeShader = ` struct ComputeInputs { - globalId: vec3, + index: vec3, + localIndex: i32, localId: vec3, workgroupId: vec3, - localIndex: i32, } struct ComputeUniforms { @@ -11,10 +11,6 @@ struct ComputeUniforms { } @group(0) @binding(0) var uniforms: ComputeUniforms; -fn processData(inputs: ComputeInputs) { - HOOK_processData(inputs); -} - @compute @workgroup_size(8, 8, 1) fn main( @builtin(global_invocation_id) globalId: vec3, @@ -23,12 +19,12 @@ fn main( @builtin(local_invocation_index) localIndex: u32 ) { var inputs: ComputeInputs; - inputs.globalId = vec3(globalId); + inputs.index = vec3(globalId); if ( - inputs.globalId.x > uniforms.uTotalCount.x || - inputs.globalId.y > uniforms.uTotalCount.y || - inputs.globalId.z > uniforms.uTotalCount.z + inputs.index.x > uniforms.uTotalCount.x || + inputs.index.y > uniforms.uTotalCount.y || + inputs.index.z > uniforms.uTotalCount.z ) { return; } @@ -37,6 +33,6 @@ fn main( inputs.workgroupId = vec3(workgroupId); inputs.localIndex = i32(localIndex); - processData(inputs); + HOOK_iteration(inputs); } `; From db483fb8b1f39d7dc316d90fc6e9edd670831f9e Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 15 Feb 2026 12:06:06 -0500 Subject: [PATCH 032/250] Transpile square brackets to set/get --- preview/index.html | 19 +++++++------ src/strands/strands_transpiler.js | 45 ++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/preview/index.html b/preview/index.html index d9c4471344..61e8fa5899 100644 --- a/preview/index.html +++ b/preview/index.html @@ -122,16 +122,16 @@ const posY_idx = idx * 2 + 1; // Read current position and velocity - let posX = positions.get(posX_idx); - let posY = positions.get(posY_idx); - let velX = velocities.get(posX_idx); - let velY = velocities.get(posY_idx); + let posX = positions[posX_idx]; + let posY = positions[posY_idx]; + let velX = velocities[posX_idx]; + let velY = velocities[posY_idx]; // Update position posX = posX + velX * deltaTime; posY = posY + velY * deltaTime; - // Bounce off boundaries (DVD logo style) + // Bounce off boundaries if (posX > bounds.x || posX < -bounds.x) { velX = -velX; posX = p.clamp(posX, -bounds.x, bounds.x); @@ -141,11 +141,10 @@ posY = p.clamp(posY, -bounds.y, bounds.y); } - // Write back - positions.set(posX_idx, posX); - positions.set(posY_idx, posY); - velocities.set(posX_idx, velX); - velocities.set(posY_idx, velY); + positions[posX_idx] = posX; + positions[posY_idx] = posY; + velocities[posX_idx] = velX; + velocities[posY_idx] = velY; }, { p, RADIUS }); // Shader for rendering bouncing circles from storage buffer diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index 91a46d2597..a01adb2dc1 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -212,7 +212,7 @@ const ASTCallbacks = { ]; let isSwizzle = swizzleSets.some(set => [...property].every(char => set.includes(char)) - ) && node.argument.type === 'MemberExpression'; + ) && node.argument.type === 'MemberExpression' && !node.argument.computed; if (isSwizzle) { node.type = 'MemberExpression'; node.object = { @@ -245,6 +245,28 @@ const ASTCallbacks = { node.arguments = []; node.type = 'CallExpression'; }, + MemberExpression(node, _state, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } + // Skip sets -- these will be converted to .set() method + // calls at the AssignmentExpression level + if (ancestors.at(-2)?.type === 'AssignmentExpression') return; + if (node.computed) { + const callee = node.object; + const member = node.property; + node.computed = undefined; + node.object = undefined; + node.callee = { + type: 'MemberExpression', + object: callee, + property: { + type: 'Identifier', + name: 'get', + } + }; + node.arguments = [member]; + node.type = 'CallExpression'; + } + }, VariableDeclarator(node, _state, ancestors) { if (ancestors.some(nodeIsUniform)) { return; } if (nodeIsUniform(node.init)) { @@ -360,6 +382,27 @@ const ASTCallbacks = { // Handle swizzle assignment to varying variable: myVarying.xyz = value // Note: node.left.object might be worldPos.getValue() due to prior Identifier transformation else if (node.left.type === 'MemberExpression') { + if (node.left.computed) { + const source = node.left; + const value = node.right; + const callee = source.object; + const member = source.property; + node.right = undefined; + node.left = undefined; + node.operator = undefined; + node.callee = { + type: 'MemberExpression', + object: callee, + property: { + type: 'Identifier', + name: 'set' + } + }; + node.arguments = [member, value]; + node.type = 'CallExpression'; + return; + } + let varyingName = null; // Check if it's a direct identifier: myVarying.xyz From 67bd05b55e90bcca0afd0d5111434bf1be544a06 Mon Sep 17 00:00:00 2001 From: dhowe Date: Mon, 16 Feb 2026 16:56:49 +0000 Subject: [PATCH 033/250] fix to #8469 in dev-2.0 --- src/type/textCore.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/type/textCore.js b/src/type/textCore.js index 345267fa4c..c1f13531b8 100644 --- a/src/type/textCore.js +++ b/src/type/textCore.js @@ -1790,6 +1790,11 @@ function textCore(p5, fn) { return { bounds, lines }; }; + Renderer.prototype._trimEnd = function (str) { + // trim trailing linebreaks and whitespace only + return str.replace(/[\s\n]+$/, ''); + }; + /* Adjust width, height of bounds based on current rectMode * @private @@ -2247,13 +2252,13 @@ function textCore(p5, fn) { testLine = `${line + words[widx]}` + splitter; testWidth = this._textWidthSingle(testLine); if (line.length > 0 && testWidth > maxWidth) { - newLines.push(line.trim()); + newLines.push(this._trimEnd(line)); line = `${words[widx]}` + splitter; } else { line = testLine; } } - newLines.push(line.trim()); + newLines.push(this._trimEnd(line)); } return newLines; }; From 0f82b9b4b5edbe22dbb9495f692290a3edfeaaa0 Mon Sep 17 00:00:00 2001 From: dhowe Date: Mon, 16 Feb 2026 16:59:47 +0000 Subject: [PATCH 034/250] Revert "fix to #8469 in dev-2.0" This reverts commit 67bd05b55e90bcca0afd0d5111434bf1be544a06. --- src/type/textCore.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/type/textCore.js b/src/type/textCore.js index c1f13531b8..345267fa4c 100644 --- a/src/type/textCore.js +++ b/src/type/textCore.js @@ -1790,11 +1790,6 @@ function textCore(p5, fn) { return { bounds, lines }; }; - Renderer.prototype._trimEnd = function (str) { - // trim trailing linebreaks and whitespace only - return str.replace(/[\s\n]+$/, ''); - }; - /* Adjust width, height of bounds based on current rectMode * @private @@ -2252,13 +2247,13 @@ function textCore(p5, fn) { testLine = `${line + words[widx]}` + splitter; testWidth = this._textWidthSingle(testLine); if (line.length > 0 && testWidth > maxWidth) { - newLines.push(this._trimEnd(line)); + newLines.push(line.trim()); line = `${words[widx]}` + splitter; } else { line = testLine; } } - newLines.push(this._trimEnd(line)); + newLines.push(line.trim()); } return newLines; }; From f258b5e537912dbfb5a627371551d314b630f40b Mon Sep 17 00:00:00 2001 From: ksen0 Date: Wed, 18 Feb 2026 22:02:12 +0100 Subject: [PATCH 035/250] Added tests --- src/math/p5.Vector.js | 42 +-- src/math/patch-vector.js | 1 - test/unit/math/p5.Vector.js | 592 ++++++++++++++++++++---------------- 3 files changed, 358 insertions(+), 277 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index bfd4b517ff..2bca04f450 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -7,21 +7,21 @@ import * as constants from '../core/constants'; /// HELPER FOR SMALLER DIMENSION PRIORITY LOGIC. /// Pending implementation as decorator. -const smallerDimensionPriorityHelper = function(dimOther, dimSelf) { - const resultDimension = Math.min(dimOther, dimSelf); - if (dimOther != dimSelf) { +const smallerDimensionPriority = function(dimOther, dimSelf) { + const minDimension = Math.min(dimOther, dimSelf); + if (dimOther !== dimSelf) { console.warn( - 'When working with two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as ' + resultDimension + 'D vectors, and any additional values of the linger vector will be ignored.', + 'When working with two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as ' + minDimension + 'D vectors, and any additional values of the linger vector will be ignored.', ); } - return resultDimension + return minDimension } class Vector { /** * The values of the N-dimensional vector. - * + * * This array of numbers that represents the vector. * Each number in the array corresponds to a different component of the vector, * like its position in different directions (e.g., x, y, z). @@ -501,11 +501,11 @@ class Vector { return this; } - const resultDimension = smallerDimensionPriorityHelper(args.length, this.dimensions); + const minDimension = smallerDimensionPriority(args.length, this.dimensions); this.values = this.values.reduce((acc, v, i) => { - if(i < resultDimension) acc[i] = this.values[i] + args[i]; + if(i < minDimension) acc[i] = this.values[i] + Number(args[i]); return acc; - }, new Array(resultDimension)); + }, new Array(minDimension)); return this; } @@ -654,14 +654,14 @@ class Vector { return this; }; - const resultDimension = smallerDimensionPriorityHelper(args.length, this.dimensions); + const minDimension = smallerDimensionPriority(args.length, this.dimensions); this.values = this.values.reduce((acc, v, i) => { // Extra check for non empty operand - if(i < resultDimension && args[i] > 0) acc[i] = this.values[i] % args[i]; + if(i < minDimension && args[i] > 0) acc[i] = this.values[i] % args[i]; else acc[i] = this.values[i] return acc; - }, new Array(resultDimension)); + }, new Array(minDimension)); return this; } @@ -806,12 +806,12 @@ class Vector { return this; } - const resultDimension = smallerDimensionPriorityHelper(args.length, this.dimensions); + const minDimension = smallerDimensionPriority(args.length, this.dimensions); this.values = this.values.reduce((acc, v, i) => { - if(i < resultDimension) acc[i] = this.values[i] - args[i]; + if(i < minDimension) acc[i] = this.values[i] - args[i]; return acc; - }, new Array(resultDimension)); + }, new Array(minDimension)); return this; } @@ -1030,12 +1030,12 @@ class Vector { return this; }; - const resultDimension = smallerDimensionPriorityHelper(args.length, this.dimensions); + const minDimension = smallerDimensionPriority(args.length, this.dimensions); this.values = this.values.reduce((acc, v, i) => { - if(i < resultDimension) acc[i] = this.values[i] * args[i]; + if(i < minDimension) acc[i] = this.values[i] * args[i]; return acc; - }, new Array(resultDimension)); + }, new Array(minDimension)); // if (args.length === 1 && args[0] instanceof Vector) { // const v = args[0]; @@ -1295,12 +1295,12 @@ class Vector { return this; }; - const resultDimension = smallerDimensionPriorityHelper(args.length, this.dimensions); + const minDimension = smallerDimensionPriority(args.length, this.dimensions); this.values = this.values.reduce((acc, v, i) => { - if(i < resultDimension) acc[i] = this.values[i] / args[i]; + if(i < minDimension) acc[i] = this.values[i] / args[i]; return acc; - }, new Array(resultDimension)); + }, new Array(minDimension)); return this; } diff --git a/src/math/patch-vector.js b/src/math/patch-vector.js index fb5717d59d..6929daae73 100644 --- a/src/math/patch-vector.js +++ b/src/math/patch-vector.js @@ -14,5 +14,4 @@ export default function patchVector(p5, fn, lifecycles){ }; }); - } diff --git a/test/unit/math/p5.Vector.js b/test/unit/math/p5.Vector.js index 41863f9898..a7b45039d3 100644 --- a/test/unit/math/p5.Vector.js +++ b/test/unit/math/p5.Vector.js @@ -1,24 +1,23 @@ -import vector from '../../../src/math/p5.Vector.js'; +import {default as vector, Vector} from '../../../src/math/p5.Vector.js'; import { vi } from 'vitest'; +// TODO add create Vector coverage + suite('p5.Vector', function () { var v; - const mockP5 = { - _validateParameters: vi.fn() - }; const mockP5Prototype = {}; beforeEach(async function () { - vector(mockP5, mockP5Prototype); + vector(mockP5Prototype); }); afterEach(function () {}); suite.todo('p5.prototype.setHeading() RADIANS', function () { beforeEach(function () { - mockP5Prototype.angleMode(mockP5.RADIANS); - v = mockP5Prototype.createVector(1, 1); + mockP5Prototype.angleMode(RADIANS); + v = new Vector(1, 1); v.setHeading(1); }); test('should have heading() value of 1 (RADIANS)', function () { @@ -28,7 +27,7 @@ suite('p5.Vector', function () { suite.todo('p5.prototype.setHeading() DEGREES', function () { beforeEach(function () { - mockP5Prototype.angleMode(mockP5.DEGREES); + mockP5Prototype.angleMode(DEGREES); v = mockP5Prototype.createVector(1, 1); v.setHeading(1); }); @@ -44,7 +43,7 @@ suite('p5.Vector', function () { v = mockP5Prototype.createVector(); }); test('should create instance of p5.Vector', function () { - assert.instanceOf(v, mockP5.Vector); + assert.instanceOf(v, Vector); }); test('should have x, y, z be initialized to 0', function () { @@ -69,6 +68,31 @@ suite('p5.Vector', function () { assert.equal(v.z, 3); }); + test('should have values be initialized to 1,2,3', function () { + assert.deepEqual(v.values, [1, 2, 3]); + }); + + test('should have dimensions initialized to 3', function () { + assert.equal(v.dimensions, 3); + }); + }); + + + suite('p5.prototype.createVector()', function () { + beforeEach(function () { + v = mockP5Prototype.createVector(); + }); + + test('should have x, y, z be initialized to 0,0,0', function () { + assert.equal(v.x, 0); + assert.equal(v.y, 0); + assert.equal(v.z, 0); + }); + + test('should have values be initialized to 0,0,0', function () { + assert.deepEqual(v.values, [0,0,0]); + }); + test('should have dimensions initialized to 3', function () { assert.equal(v.dimensions, 3); }); @@ -76,10 +100,10 @@ suite('p5.Vector', function () { suite('new p5.Vector()', function () { beforeEach(function () { - v = new mockP5.Vector(); + v = new Vector(); }); test('should set constant to DEGREES', function () { - assert.instanceOf(v, mockP5.Vector); + assert.instanceOf(v, Vector); }); test('should have x, y, z be initialized to 0', function () { @@ -91,7 +115,7 @@ suite('p5.Vector', function () { suite('new p5.Vector(1, 2, 3)', function () { beforeEach(function () { - v = new mockP5.Vector(1, 2, 3); + v = new Vector(1, 2, 3); }); test('should have x, y, z be initialized to 1,2,3', function () { @@ -103,7 +127,7 @@ suite('p5.Vector', function () { suite('new p5.Vector(1,2,undefined)', function () { beforeEach(function () { - v = new mockP5.Vector(1, 2, undefined); + v = new Vector(1, 2, undefined); }); test('should have x, y, z be initialized to 1,2,0', function () { @@ -116,13 +140,13 @@ suite('p5.Vector', function () { suite('rotate', function () { suite('p5.Vector.prototype.rotate() [INSTANCE]', function () { test('should return the same object', function () { - v = new mockP5.Vector(0, 1); + v = new Vector(0, 1); expect(v.rotate(Math.PI)).to.eql(v); }); suite.todo('radians', function () { beforeEach(function () { - mockP5Prototype.angleMode(mockP5.RADIANS); + mockP5Prototype.angleMode(RADIANS); }); test('should rotate the vector [0, 1, 0] by pi radians to [0, -1, 0]', function () { @@ -152,7 +176,7 @@ suite('p5.Vector', function () { suite.todo('degrees', function () { beforeEach(function () { - mockP5Prototype.angleMode(mockP5.DEGREES); + mockP5Prototype.angleMode(DEGREES); }); test('should rotate the vector [0, 1, 0] by 180 degrees to [0, -1, 0]', function () { @@ -175,12 +199,12 @@ suite('p5.Vector', function () { suite.todo('p5.Vector.rotate() [CLASS]', function () { beforeEach(function () { - mockP5Prototype.angleMode(mockP5.RADIANS); + mockP5Prototype.angleMode(RADIANS); }); test('should not change the original object', function () { v = mockP5Prototype.createVector(1, 0, 0); - mockP5.Vector.rotate(v, Math.PI / 2); + Vector.rotate(v, Math.PI / 2); expect(v.x).to.equal(1); expect(v.y).to.equal(0); expect(v.z).to.equal(0); @@ -188,7 +212,7 @@ suite('p5.Vector', function () { test('should rotate the vector [0, 1, 0] by pi radians to [0, -1, 0]', function () { v = mockP5Prototype.createVector(0, 1, 0); - const v1 = mockP5.Vector.rotate(v, Math.PI); + const v1 = Vector.rotate(v, Math.PI); expect(v1.x).to.be.closeTo(0, 0.01); expect(v1.y).to.be.closeTo(-1, 0.01); expect(v1.z).to.be.closeTo(0, 0.01); @@ -196,7 +220,7 @@ suite('p5.Vector', function () { test('should rotate the vector [1, 0, 0] by -pi/2 radians to [0, -1, 0]', function () { v = mockP5Prototype.createVector(1, 0, 0); - const v1 = mockP5.Vector.rotate(v, -Math.PI / 2); + const v1 = Vector.rotate(v, -Math.PI / 2); expect(v1.x).to.be.closeTo(0, 0.01); expect(v1.y).to.be.closeTo(-1, 0.01); expect(v1.z).to.be.closeTo(0, 0.01); @@ -207,8 +231,8 @@ suite('p5.Vector', function () { suite('angleBetween', function () { let v1, v2; beforeEach(function () { - v1 = new mockP5.Vector(1, 0, 0); - v2 = new mockP5.Vector(2, 2, 0); + v1 = new Vector(1, 0, 0); + v2 = new Vector(2, 2, 0); }); suite('p5.Vector.prototype.angleBetween() [INSTANCE]', function () { @@ -218,60 +242,60 @@ suite('p5.Vector', function () { }); test('should not trip on rounding issues in 2D space', function () { - v1 = new mockP5.Vector(-11, -20); - v2 = new mockP5.Vector(-5.5, -10); - const v3 = new mockP5.Vector(5.5, 10); + v1 = new Vector(-11, -20); + v2 = new Vector(-5.5, -10); + const v3 = new Vector(5.5, 10); expect(v1.angleBetween(v2)).to.be.closeTo(0, 0.00001); expect(v1.angleBetween(v3)).to.be.closeTo(Math.PI, 0.00001); }); test('should not trip on rounding issues in 3D space', function () { - v1 = new mockP5.Vector(1, 1.1, 1.2); - v2 = new mockP5.Vector(2, 2.2, 2.4); + v1 = new Vector(1, 1.1, 1.2); + v2 = new Vector(2, 2.2, 2.4); expect(v1.angleBetween(v2)).to.be.closeTo(0, 0.00001); }); test('should return NaN for zero vector', function () { - v1 = new mockP5.Vector(0, 0, 0); - v2 = new mockP5.Vector(2, 3, 4); + v1 = new Vector(0, 0, 0); + v2 = new Vector(2, 3, 4); expect(v1.angleBetween(v2)).to.be.NaN; expect(v2.angleBetween(v1)).to.be.NaN; }); test.todo('between [1,0,0] and [1,0,0] should be 0 degrees', function () { - mockP5Prototype.angleMode(mockP5.DEGREES); - v1 = new mockP5.Vector(1, 0, 0); - v2 = new mockP5.Vector(1, 0, 0); + mockP5Prototype.angleMode(DEGREES); + v1 = new Vector(1, 0, 0); + v2 = new Vector(1, 0, 0); expect(v1.angleBetween(v2)).to.equal(0); }); test.todo( 'between [0,3,0] and [0,-3,0] should be 180 degrees', function () { - mockP5Prototype.angleMode(mockP5.DEGREES); - v1 = new mockP5.Vector(0, 3, 0); - v2 = new mockP5.Vector(0, -3, 0); + mockP5Prototype.angleMode(DEGREES); + v1 = new Vector(0, 3, 0); + v2 = new Vector(0, -3, 0); expect(v1.angleBetween(v2)).to.be.closeTo(180, 0.01); } ); test('between [1,0,0] and [2,2,0] should be 1/4 PI radians', function () { - v1 = new mockP5.Vector(1, 0, 0); - v2 = new mockP5.Vector(2, 2, 0); + v1 = new Vector(1, 0, 0); + v2 = new Vector(2, 2, 0); expect(v1.angleBetween(v2)).to.be.closeTo(Math.PI / 4, 0.01); expect(v2.angleBetween(v1)).to.be.closeTo((-1 * Math.PI) / 4, 0.01); }); test('between [2,0,0] and [-2,0,0] should be PI radians', function () { - v1 = new mockP5.Vector(2, 0, 0); - v2 = new mockP5.Vector(-2, 0, 0); + v1 = new Vector(2, 0, 0); + v2 = new Vector(-2, 0, 0); expect(v1.angleBetween(v2)).to.be.closeTo(Math.PI, 0.01); }); test('between [2,0,0] and [-2,-2,0] should be -3/4 PI radians ', function () { - v1 = new mockP5.Vector(2, 0, 0); - v2 = new mockP5.Vector(-2, -2, 0); + v1 = new Vector(2, 0, 0); + v2 = new Vector(-2, -2, 0); expect(v1.angleBetween(v2)).to.be.closeTo( -1 * (Math.PI / 2 + Math.PI / 4), 0.01 @@ -279,8 +303,8 @@ suite('p5.Vector', function () { }); test('between [-2,-2,0] and [2,0,0] should be 3/4 PI radians', function () { - v1 = new mockP5.Vector(-2, -2, 0); - v2 = new mockP5.Vector(2, 0, 0); + v1 = new Vector(-2, -2, 0); + v2 = new Vector(2, 0, 0); expect(v1.angleBetween(v2)).to.be.closeTo( Math.PI / 2 + Math.PI / 4, 0.01 @@ -288,40 +312,40 @@ suite('p5.Vector', function () { }); test('For the same vectors, the angle between them should always be 0.', function () { - v1 = new mockP5.Vector(288, 814); - v2 = new mockP5.Vector(288, 814); + v1 = new Vector(288, 814); + v2 = new Vector(288, 814); expect(v1.angleBetween(v2)).to.equal(0); }); test('The angle between vectors pointing in opposite is always PI.', function () { - v1 = new mockP5.Vector(219, 560); - v2 = new mockP5.Vector(-219, -560); + v1 = new Vector(219, 560); + v2 = new Vector(-219, -560); expect(v1.angleBetween(v2)).to.be.closeTo(Math.PI, 0.0000001); }); }); suite('p5.Vector.angleBetween() [CLASS]', function () { test('should return NaN for zero vector', function () { - v1 = new mockP5.Vector(0, 0, 0); - v2 = new mockP5.Vector(2, 3, 4); - expect(mockP5.Vector.angleBetween(v1, v2)).to.be.NaN; - expect(mockP5.Vector.angleBetween(v2, v1)).to.be.NaN; + v1 = new Vector(0, 0, 0); + v2 = new Vector(2, 3, 4); + expect(Vector.angleBetween(v1, v2)).to.be.NaN; + expect(Vector.angleBetween(v2, v1)).to.be.NaN; }); test.todo( 'between [1,0,0] and [0,-1,0] should be -90 degrees', function () { - mockP5Prototype.angleMode(mockP5.DEGREES); - v1 = new mockP5.Vector(1, 0, 0); - v2 = new mockP5.Vector(0, -1, 0); - expect(mockP5.Vector.angleBetween(v1, v2)).to.be.closeTo(-90, 0.01); + mockP5Prototype.angleMode(DEGREES); + v1 = new Vector(1, 0, 0); + v2 = new Vector(0, -1, 0); + expect(Vector.angleBetween(v1, v2)).to.be.closeTo(-90, 0.01); } ); test('between [0,3,0] and [0,-3,0] should be PI radians', function () { - v1 = new mockP5.Vector(0, 3, 0); - v2 = new mockP5.Vector(0, -3, 0); - expect(mockP5.Vector.angleBetween(v1, v2)).to.be.closeTo(Math.PI, 0.01); + v1 = new Vector(0, 3, 0); + v2 = new Vector(0, -3, 0); + expect(Vector.angleBetween(v1, v2)).to.be.closeTo(Math.PI, 0.01); }); }); }); @@ -329,7 +353,7 @@ suite('p5.Vector', function () { suite('set()', function () { suite('with p5.Vector', function () { test("should have x, y, z be initialized to the vector's x, y, z", function () { - v.set(new mockP5.Vector(2, 5, 6)); + v.set(new Vector(2, 5, 6)); expect(v.x).to.eql(2); expect(v.y).to.eql(5); expect(v.z).to.eql(6); @@ -364,7 +388,7 @@ suite('p5.Vector', function () { suite('copy', function () { beforeEach(function () { - v = new mockP5.Vector(2, 3, 4); + v = new Vector(2, 3, 4); }); suite('p5.Vector.prototype.copy() [INSTANCE]', function () { @@ -383,12 +407,12 @@ suite('p5.Vector', function () { suite('p5.Vector.copy() [CLASS]', function () { test('should not return the same instance', function () { - var newObject = mockP5.Vector.copy(v); + var newObject = Vector.copy(v); expect(newObject).to.not.equal(v); }); test("should return the passed object's x, y, z", function () { - var newObject = mockP5.Vector.copy(v); + var newObject = Vector.copy(v); expect(newObject.x).to.eql(2); expect(newObject.y).to.eql(3); expect(newObject.z).to.eql(4); @@ -398,12 +422,12 @@ suite('p5.Vector', function () { suite('add()', function () { beforeEach(function () { - v = new mockP5.Vector(0, 0, 0); + v = new Vector(0, 0, 0); }); suite('with p5.Vector', function () { test('should add x, y, z from the vector argument', function () { - v.add(new mockP5.Vector(1, 5, 6)); + v.add(new Vector(1, 5, 6)); expect(v.x).to.eql(1); expect(v.y).to.eql(5); expect(v.z).to.eql(6); @@ -457,9 +481,9 @@ suite('p5.Vector', function () { suite('p5.Vector.add(v1, v2)', function () { var v1, v2, res; beforeEach(function () { - v1 = new mockP5.Vector(2, 0, 3); - v2 = new mockP5.Vector(0, 1, 3); - res = mockP5.Vector.add(v1, v2); + v1 = new Vector(2, 0, 3); + v2 = new Vector(0, 1, 3); + res = Vector.add(v1, v2); }); test('should return neither v1 nor v2', function () { @@ -477,7 +501,7 @@ suite('p5.Vector', function () { suite('rem()', function () { beforeEach(function () { - v = new mockP5.Vector(3, 4, 5); + v = new Vector(3, 4, 5); }); test('should give same vector if nothing passed as parameter', function () { @@ -510,28 +534,28 @@ suite('p5.Vector', function () { suite('with p5.Vector', function () { test('should return correct output if only one component is non-zero', function () { - v.rem(new mockP5.Vector(0, 0, 4)); + v.rem(new Vector(0, 0, 4)); expect(v.x).to.eql(3); expect(v.y).to.eql(4); expect(v.z).to.eql(1); }); test('should return correct output if x component is zero', () => { - v.rem(new mockP5.Vector(0, 3, 4)); + v.rem(new Vector(0, 3, 4)); expect(v.x).to.eql(3); expect(v.y).to.eql(1); expect(v.z).to.eql(1); }); test('should return correct output if all components are non-zero', () => { - v.rem(new mockP5.Vector(2, 3, 4)); + v.rem(new Vector(2, 3, 4)); expect(v.x).to.eql(1); expect(v.y).to.eql(1); expect(v.z).to.eql(1); }); test('should return same vector if all components are zero', () => { - v.rem(new mockP5.Vector(0, 0, 0)); + v.rem(new Vector(0, 0, 0)); expect(v.x).to.eql(3); expect(v.y).to.eql(4); expect(v.z).to.eql(5); @@ -541,10 +565,10 @@ suite('p5.Vector', function () { suite('with negative vectors', function () { let v; beforeEach(function () { - v = new mockP5.Vector(-15, -5, -2); + v = new Vector(-15, -5, -2); }); test('should return correct output', () => { - v.rem(new mockP5.Vector(2, 3, 3)); + v.rem(new Vector(2, 3, 3)); expect(v.x).to.eql(-1); expect(v.y).to.eql(-2); expect(v.z).to.eql(-2); @@ -583,9 +607,9 @@ suite('p5.Vector', function () { suite('p5.Vector.rem(v1,v2)', function () { let v1, v2, res; beforeEach(function () { - v1 = new mockP5.Vector(2, 3, 4); - v2 = new mockP5.Vector(1, 2, 3); - res = mockP5.Vector.rem(v1, v2); + v1 = new Vector(2, 3, 4); + v2 = new Vector(1, 2, 3); + res = Vector.rem(v1, v2); }); test('should return neither v1 nor v2', function () { @@ -605,14 +629,11 @@ suite('p5.Vector', function () { beforeEach(function () { v.x = 0; v.y = 0; - v.z = 0; }); suite('with p5.Vector', function () { test('should sub x, y, z from the vector argument', function () { - v.sub(new mockP5.Vector(2, 5, 6)); - expect(v.x).to.eql(-2); - expect(v.y).to.eql(-5); - expect(v.z).to.eql(-6); + v.sub(new Vector(2, 5)); + assert.deepEqual(v.values, [-2, -5]); }); }); @@ -626,11 +647,9 @@ suite('p5.Vector', function () { }); }); - test("should subtract from the array's 0,1,2 index", function () { + test("should subtract from the array's 0, 1, 2 index", function () { v.sub([2, 5, 6]); - expect(v.x).to.eql(-2); - expect(v.y).to.eql(-5); - expect(v.z).to.eql(-6); + expect(v.values).to.eql([-2, -5]); }); }); @@ -645,19 +664,19 @@ suite('p5.Vector', function () { suite('sub(2,3,4)', function () { test('should subtract the x, y, z components', function () { - v.sub(5, 5, 5); + v.sub(5, 5); expect(v.x).to.eql(-5); expect(v.y).to.eql(-5); - expect(v.z).to.eql(-5); + //expect(v.z).to.eql(-5); }); }); suite('p5.Vector.sub(v1, v2)', function () { var v1, v2, res; beforeEach(function () { - v1 = new mockP5.Vector(2, 0, 3); - v2 = new mockP5.Vector(0, 1, 3); - res = mockP5.Vector.sub(v1, v2); + v1 = new Vector(2, 0, 3); + v2 = new Vector(0, 1, 3); + res = Vector.sub(v1, v2); }); test('should return neither v1 nor v2', function () { @@ -675,7 +694,7 @@ suite('p5.Vector', function () { suite('mult()', function () { beforeEach(function () { - v = new mockP5.Vector(1, 1, 1); + v = new Vector(1, 1, 1); }); test('should return the same object', function () { @@ -705,11 +724,20 @@ suite('p5.Vector', function () { }); }); + suite('with arglist', function () { + test('multiply the x, y, z with the scalar', function () { + v.mult(2, 3, 4); + expect(v.x).to.eql(2); + expect(v.y).to.eql(3); + expect(v.z).to.eql(4); + }); + }); + suite('v0.mult(v1)', function () { var v0, v1; beforeEach(function () { - v0 = new mockP5.Vector(1, 2, 3); - v1 = new mockP5.Vector(2, 3, 4); + v0 = new Vector(1, 2, 3); + v1 = new Vector(2, 3, 4); v0.mult(v1); }); @@ -723,7 +751,7 @@ suite('p5.Vector', function () { suite('v0.mult(arr)', function () { var v0, arr; beforeEach(function () { - v0 = new mockP5.Vector(1, 2, 3); + v0 = new Vector(1, 2, 3); arr = [2, 3, 4]; v0.mult(arr); }); @@ -738,8 +766,8 @@ suite('p5.Vector', function () { suite('p5.Vector.mult(v, n)', function () { var v, res; beforeEach(function () { - v = new mockP5.Vector(1, 2, 3); - res = mockP5.Vector.mult(v, 4); + v = new Vector(1, 2, 3); + res = Vector.mult(v, 4); }); test('should return a new p5.Vector', function () { @@ -756,9 +784,9 @@ suite('p5.Vector', function () { suite('p5.Vector.mult(v, v', function () { var v0, v1, res; beforeEach(function () { - v0 = new mockP5.Vector(1, 2, 3); - v1 = new mockP5.Vector(2, 3, 4); - res = mockP5.Vector.mult(v0, v1); + v0 = new Vector(1, 2, 3); + v1 = new Vector(2, 3, 4); + res = Vector.mult(v0, v1); }); test('should return new vector from component wise multiplication', function () { @@ -771,9 +799,9 @@ suite('p5.Vector', function () { suite('p5.Vector.mult(v, arr', function () { var v0, arr, res; beforeEach(function () { - v0 = new mockP5.Vector(1, 2, 3); + v0 = new Vector(1, 2, 3); arr = [2, 3, 4]; - res = mockP5.Vector.mult(v0, arr); + res = Vector.mult(v0, arr); }); test('should return new vector from component wise multiplication with an array', function () { @@ -786,7 +814,7 @@ suite('p5.Vector', function () { suite('div()', function () { beforeEach(function () { - v = new mockP5.Vector(1, 1, 1); + v = new Vector(1, 1, 1); }); test('should return the same object', function () { @@ -823,11 +851,20 @@ suite('p5.Vector', function () { }); }); + suite('with arglist', function () { + test('multiply the x, y, z with the scalar', function () { + v.div(2, 3, 4); + expect(v.x).to.be.closeTo(0.5, 0.01); + expect(v.y).to.be.closeTo(0.333, 0.01); + expect(v.z).to.be.closeTo(0.25, 0.01); + }); + }); + suite('p5.Vector.div(v, n)', function () { var v, res; beforeEach(function () { - v = new mockP5.Vector(1, 1, 1); - res = mockP5.Vector.div(v, 4); + v = new Vector(1, 1, 1); + res = Vector.div(v, 4); }); test('should not be undefined', function () { @@ -848,10 +885,10 @@ suite('p5.Vector', function () { suite('v0.div(v1)', function () { var v0, v1, v2, v3; beforeEach(function () { - v0 = new mockP5.Vector(2, 6, 9); - v1 = new mockP5.Vector(2, 2, 3); - v2 = new mockP5.Vector(1, 1, 1); - v3 = new mockP5.Vector(0, 0, 0); + v0 = new Vector(2, 6, 9); + v1 = new Vector(2, 2, 3); + v2 = new Vector(1, 1, 1); + v3 = new Vector(0, 0, 0); v0.div(v1); }); @@ -870,8 +907,8 @@ suite('p5.Vector', function () { }); test('should work on 2D vectors', function () { - const v = new mockP5.Vector(1, 1); - const divisor = new mockP5.Vector(2, 2); + const v = new Vector(1, 1); + const divisor = new Vector(2, 2); v.div(divisor); expect(v.x).to.eql(0.5); expect(v.y).to.eql(0.5); @@ -879,8 +916,8 @@ suite('p5.Vector', function () { }); test('should work when the dividend has 0', function () { - const v = new mockP5.Vector(1, 0); - const divisor = new mockP5.Vector(2, 2); + const v = new Vector(1, 0); + const divisor = new Vector(2, 2); v.div(divisor); expect(v.x).to.eql(0.5); expect(v.y).to.eql(0); @@ -888,8 +925,8 @@ suite('p5.Vector', function () { }); test('should do nothing when the divisor has 0', function () { - const v = new mockP5.Vector(1, 1); - const divisor = new mockP5.Vector(0, 2); + const v = new Vector(1, 1); + const divisor = new Vector(0, 2); v.div(divisor); expect(v.x).to.eql(1); expect(v.y).to.eql(1); @@ -900,8 +937,8 @@ suite('p5.Vector', function () { suite('v0.div(arr)', function () { var v0, v1, arr; beforeEach(function () { - v0 = new mockP5.Vector(2, 6, 9); - v1 = new mockP5.Vector(1, 1, 1); + v0 = new Vector(2, 6, 9); + v1 = new Vector(1, 1, 1); arr = [2, 2, 3]; v0.div(arr); }); @@ -923,9 +960,9 @@ suite('p5.Vector', function () { suite('p5.Vector.div(v, v', function () { var v0, v1, res; beforeEach(function () { - v0 = new mockP5.Vector(2, 6, 9); - v1 = new mockP5.Vector(2, 2, 3); - res = mockP5.Vector.div(v0, v1); + v0 = new Vector(2, 6, 9); + v1 = new Vector(2, 2, 3); + res = Vector.div(v0, v1); }); test('should return new vector from component wise division', function () { @@ -938,9 +975,9 @@ suite('p5.Vector', function () { suite('p5.Vector.div(v, arr', function () { var v0, arr, res; beforeEach(function () { - v0 = new mockP5.Vector(2, 6, 9); + v0 = new Vector(2, 6, 9); arr = [2, 2, 3]; - res = mockP5.Vector.div(v0, arr); + res = Vector.div(v0, arr); }); test('should return new vector from component wise division with an array', function () { @@ -951,6 +988,52 @@ suite('p5.Vector', function () { }); }); + + suite('smaller dimension', function () { + let v0, v1, v2, v3; + beforeEach(function () { + v0 = new Vector(); + v1 = new Vector([1]); + v2 = new Vector([2, 3]); + v3 = new Vector([4, 5, 6]); + }); + + test('should be prioritized in add()', function () { + assert.deepEqual(v1.add(v2).values, [2]); + assert.deepEqual(v1.add(v2).dimension, 1); + assert.deepEqual(v3.add(v2).values, [8,15]); + assert.deepEqual(v3.add(v2).dimension, 2); + }); + + test('should be prioritized in sub()', function () { + assert.deepEqual(v1.sub(v2).values, [-1]); + assert.deepEqual(v1.sub(v2).dimension, 1); + assert.deepEqual(v3.sub(v2).values, [2, 2]); + assert.deepEqual(v3.sub(v2).dimension, 2); + }); + + test('should be prioritized in mult()', function () { + assert.deepEqual(v1.mult(v2).values, [2]); + assert.deepEqual(v1.mult(v2).dimension, 1); + assert.deepEqual(v3.mult(v2).values, [8, 15]); + assert.deepEqual(v3.mult(v2).dimension, 2); + }); + + test('should be prioritized in div()', function () { + assert.deepEqual(v1.div(v2).values, [1/2]); + assert.deepEqual(v1.div(v2).dimension, 1); + assert.deepEqual(v3.div(v2).values, [2, 5/3]); + assert.deepEqual(v3.div(v2).dimension, 2); + }); + + test('should be prioritized in rem()', function () { + assert.deepEqual(v1.rem(v2).values, [1]); + assert.deepEqual(v1.rem(v2).dimension, 1); + assert.deepEqual(v3.rem(v2).values, [0, 2]); + assert.deepEqual(v3.rem(v2).dimension, 2); + }); + }); + suite('dot', function () { beforeEach(function () { v.x = 1; @@ -959,12 +1042,12 @@ suite('p5.Vector', function () { }); test('should return a number', function () { - expect(typeof v.dot(new mockP5.Vector()) === 'number').to.eql(true); + expect(typeof v.dot(new Vector()) === 'number').to.eql(true); }); suite('with p5.Vector', function () { test('should be the dot product of the vector', function () { - expect(v.dot(new mockP5.Vector(2, 2))).to.eql(4); + expect(v.dot(new Vector(2, 2))).to.eql(4); }); }); @@ -981,9 +1064,9 @@ suite('p5.Vector', function () { suite('p5.Vector.dot(v, n)', function () { var v1, v2, res; beforeEach(function () { - v1 = new mockP5.Vector(1, 1, 1); - v2 = new mockP5.Vector(2, 3, 4); - res = mockP5.Vector.dot(v1, v2); + v1 = new Vector(1, 1, 1); + v2 = new Vector(2, 3, 4); + res = Vector.dot(v1, v2); }); test('should return a number', function () { @@ -1005,12 +1088,12 @@ suite('p5.Vector', function () { }); test('should return a new product', function () { - expect(v.cross(new mockP5.Vector())).to.not.eql(v); + expect(v.cross(new Vector())).to.not.eql(v); }); suite('with p5.Vector', function () { test('should cross x, y, z from the vector argument', function () { - res = v.cross(new mockP5.Vector(2, 5, 6)); + res = v.cross(new Vector(2, 5, 6)); expect(res.x).to.eql(1); //this.y * v.z - this.z * v.y expect(res.y).to.eql(-4); //this.z * v.x - this.x * v.z expect(res.z).to.eql(3); //this.x * v.y - this.y * v.x @@ -1020,9 +1103,9 @@ suite('p5.Vector', function () { suite('p5.Vector.cross(v1, v2)', function () { var v1, v2, res; beforeEach(function () { - v1 = new mockP5.Vector(3, 6, 9); - v2 = new mockP5.Vector(1, 1, 1); - res = mockP5.Vector.cross(v1, v2); + v1 = new Vector(3, 6, 9); + v2 = new Vector(1, 1, 1); + res = Vector.cross(v1, v2); }); test('should not be undefined', function () { @@ -1045,9 +1128,9 @@ suite('p5.Vector', function () { suite('dist', function () { var b, c; beforeEach(function () { - v = new mockP5.Vector(0, 0, 1); - b = new mockP5.Vector(0, 0, 5); - c = new mockP5.Vector(3, 4, 1); + v = new Vector(0, 0, 1); + b = new Vector(0, 0, 5); + c = new Vector(3, 4, 1); }); test('should return a number', function () { @@ -1070,23 +1153,23 @@ suite('p5.Vector', function () { suite('p5.Vector.dist(v1, v2)', function () { var v1, v2; beforeEach(function () { - v1 = new mockP5.Vector(0, 0, 0); - v2 = new mockP5.Vector(0, 3, 4); + v1 = new Vector(0, 0, 0); + v2 = new Vector(0, 3, 4); }); test('should return a number', function () { - expect(typeof mockP5.Vector.dist(v1, v2) === 'number').to.eql(true); + expect(typeof Vector.dist(v1, v2) === 'number').to.eql(true); }); test('should be commutative', function () { - expect(mockP5.Vector.dist(v1, v2)).to.eql(mockP5.Vector.dist(v2, v1)); + expect(Vector.dist(v1, v2)).to.eql(Vector.dist(v2, v1)); }); }); suite('normalize', function () { suite('p5.Vector.prototype.normalize() [INSTANCE]', function () { beforeEach(function () { - v = new mockP5.Vector(1, 1, 1); + v = new Vector(1, 1, 1); }); test('should return the same object', function () { @@ -1117,8 +1200,8 @@ suite('p5.Vector', function () { suite('p5.Vector.normalize(v) [CLASS]', function () { var res; beforeEach(function () { - v = new mockP5.Vector(1, 0, 0); - res = mockP5.Vector.normalize(v); + v = new Vector(1, 0, 0); + res = Vector.normalize(v); }); test('should not be undefined', function () { @@ -1139,7 +1222,7 @@ suite('p5.Vector', function () { v.x = 2; v.y = 2; v.z = 1; - res = mockP5.Vector.normalize(v); + res = Vector.normalize(v); expect(res.x).to.be.closeTo(0.6666, 0.01); expect(res.y).to.be.closeTo(0.6666, 0.01); expect(res.z).to.be.closeTo(0.3333, 0.01); @@ -1151,7 +1234,7 @@ suite('p5.Vector', function () { let v; beforeEach(function () { - v = new mockP5.Vector(5, 5, 5); + v = new Vector(5, 5, 5); }); suite('p5.Vector.prototype.limit() [INSTANCE]', function () { @@ -1180,12 +1263,12 @@ suite('p5.Vector', function () { suite('p5.Vector.limit() [CLASS]', function () { test('should not return the same object', function () { - expect(mockP5.Vector.limit(v)).to.not.equal(v); + expect(Vector.limit(v)).to.not.equal(v); }); suite('with a vector larger than the limit', function () { test('should limit the vector', function () { - const res = mockP5.Vector.limit(v, 1); + const res = Vector.limit(v, 1); expect(res.x).to.be.closeTo(0.5773, 0.01); expect(res.y).to.be.closeTo(0.5773, 0.01); expect(res.z).to.be.closeTo(0.5773, 0.01); @@ -1194,7 +1277,7 @@ suite('p5.Vector', function () { suite('with a vector smaller than the limit', function () { test('should not limit the vector', function () { - const res = mockP5.Vector.limit(v, 8.67); + const res = Vector.limit(v, 8.67); expect(res.x).to.eql(5); expect(res.y).to.eql(5); expect(res.z).to.eql(5); @@ -1203,8 +1286,8 @@ suite('p5.Vector', function () { suite('when given a target vector', function () { test('should store limited vector in the target', function () { - const target = new mockP5.Vector(0, 0, 0); - mockP5.Vector.limit(v, 1, target); + const target = new Vector(0, 0, 0); + Vector.limit(v, 1, target); expect(target.x).to.be.closeTo(0.5773, 0.01); expect(target.y).to.be.closeTo(0.5773, 0.01); expect(target.z).to.be.closeTo(0.5773, 0.01); @@ -1217,7 +1300,7 @@ suite('p5.Vector', function () { let v; beforeEach(function () { - v = new mockP5.Vector(1, 0, 0); + v = new Vector(1, 0, 0); }); suite('p5.Vector.setMag() [INSTANCE]', function () { @@ -1245,11 +1328,11 @@ suite('p5.Vector', function () { suite('p5.Vector.prototype.setMag() [CLASS]', function () { test('should not return the same object', function () { - expect(mockP5.Vector.setMag(v, 2)).to.not.equal(v); + expect(Vector.setMag(v, 2)).to.not.equal(v); }); test('should set the magnitude of the vector', function () { - const res = mockP5.Vector.setMag(v, 4); + const res = Vector.setMag(v, 4); expect(res.x).to.eql(4); expect(res.y).to.eql(0); expect(res.z).to.eql(0); @@ -1259,7 +1342,7 @@ suite('p5.Vector', function () { v.x = 2; v.y = 3; v.z = -6; - const res = mockP5.Vector.setMag(v, 14); + const res = Vector.setMag(v, 14); expect(res.x).to.eql(4); expect(res.y).to.eql(6); expect(res.z).to.eql(-12); @@ -1267,8 +1350,8 @@ suite('p5.Vector', function () { suite('when given a target vector', function () { test('should set the magnitude on the target', function () { - const target = new mockP5.Vector(0, 1, 0); - const res = mockP5.Vector.setMag(v, 4, target); + const target = new Vector(0, 1, 0); + const res = Vector.setMag(v, 4, target); expect(target).to.equal(res); expect(target.x).to.eql(4); expect(target.y).to.eql(0); @@ -1280,7 +1363,7 @@ suite('p5.Vector', function () { suite('heading', function () { beforeEach(function () { - v = new mockP5.Vector(); + v = new Vector(); }); suite('p5.Vector.prototype.heading() [INSTANCE]', function () { @@ -1311,7 +1394,7 @@ suite('p5.Vector', function () { suite.todo('with `angleMode(DEGREES)`', function () { beforeEach(function () { - mockP5Prototype.angleMode(mockP5.DEGREES); + mockP5Prototype.angleMode(DEGREES); }); test('heading for vector pointing right is 0', function () { @@ -1339,28 +1422,28 @@ suite('p5.Vector', function () { suite('p5.Vector.heading() [CLASS]', function () { test('should return a number', function () { - expect(typeof mockP5.Vector.heading(v) === 'number').to.eql(true); + expect(typeof Vector.heading(v) === 'number').to.eql(true); }); test('heading for vector pointing right is 0', function () { v.x = 1; v.y = 0; v.z = 0; - expect(mockP5.Vector.heading(v)).to.be.closeTo(0, 0.01); + expect(Vector.heading(v)).to.be.closeTo(0, 0.01); }); test('heading for vector pointing down is PI/2', function () { v.x = 0; v.y = 1; v.z = 0; - expect(mockP5.Vector.heading(v)).to.be.closeTo(Math.PI / 2, 0.01); + expect(Vector.heading(v)).to.be.closeTo(Math.PI / 2, 0.01); }); test('heading for vector pointing left is PI', function () { v.x = -1; v.y = 0; v.z = 0; - expect(mockP5.Vector.heading(v)).to.be.closeTo(Math.PI, 0.01); + expect(Vector.heading(v)).to.be.closeTo(Math.PI, 0.01); }); }); }); @@ -1370,14 +1453,13 @@ suite('p5.Vector', function () { expect(v.lerp()).to.eql(v); }); - // TODO PEND: ADD BACK IN - // suite('with p5.Vector', function() { - // test('should call lerp with 4 arguments', function() { - // spyOn(v, 'lerp').andCallThrough(); - // v.lerp(new p5.Vector(1,2,3), 1); - // expect(v.lerp).toHaveBeenCalledWith(1, 2, 3, 1); - // }); - // }); + suite('with p5.Vector', function() { + test('should call lerp with 4 arguments', function() { + vi.spyOn(v, 'lerp'); + v.lerp(new Vector(1,2,3), 1); + expect(v.lerp).toHaveBeenCalledWith(1, 2, 3, 1); + }); + }); suite('with x, y, z, amt', function () { beforeEach(function () { @@ -1416,9 +1498,9 @@ suite('p5.Vector', function () { suite('p5.Vector.lerp(v1, v2, amt)', function () { var res, v1, v2; beforeEach(function () { - v1 = new mockP5.Vector(0, 0, 0); - v2 = new mockP5.Vector(2, 2, 2); - res = mockP5.Vector.lerp(v1, v2, 0.5); + v1 = new Vector(0, 0, 0); + v2 = new Vector(2, 2, 2); + res = Vector.lerp(v1, v2, 0.5); }); test('should not be undefined', function () { @@ -1426,7 +1508,7 @@ suite('p5.Vector', function () { }); test('should be a p5.Vector', function () { - expect(res).to.be.an.instanceof(mockP5.Vector); + expect(res).to.be.an.instanceof(Vector); }); test('should return neither v1 nor v2', function () { @@ -1445,7 +1527,7 @@ suite('p5.Vector', function () { var w; beforeEach(function () { v.set(1, 2, 3); - w = new mockP5.Vector(4, 6, 8); + w = new Vector(4, 6, 8); }); test('if amt is 0, returns original vector', function () { @@ -1496,9 +1578,9 @@ suite('p5.Vector', function () { suite('p5.Vector.slerp(v1, v2, amt)', function () { var res, v1, v2; beforeEach(function () { - v1 = new mockP5.Vector(1, 0, 0); - v2 = new mockP5.Vector(0, 0, 1); - res = mockP5.Vector.slerp(v1, v2, 1 / 3); + v1 = new Vector(1, 0, 0); + v2 = new Vector(0, 0, 1); + res = Vector.slerp(v1, v2, 1 / 3); }); test('should not be undefined', function () { @@ -1506,7 +1588,7 @@ suite('p5.Vector', function () { }); test('should be a p5.Vector', function () { - expect(res).to.be.an.instanceof(mockP5.Vector); + expect(res).to.be.an.instanceof(Vector); }); test('should return neither v1 nor v2', function () { @@ -1521,14 +1603,14 @@ suite('p5.Vector', function () { }); test('Make sure the interpolation in -1/3 is correct', function () { - mockP5.Vector.slerp(v1, v2, -1 / 3, res); + Vector.slerp(v1, v2, -1 / 3, res); expect(res.x).to.be.closeTo(Math.cos(-Math.PI / 6), 0.00001); expect(res.y).to.be.closeTo(0, 0.00001); expect(res.z).to.be.closeTo(Math.sin(-Math.PI / 6), 0.00001); }); test('Make sure the interpolation in 5/3 is correct', function () { - mockP5.Vector.slerp(v1, v2, 5 / 3, res); + Vector.slerp(v1, v2, 5 / 3, res); expect(res.x).to.be.closeTo(Math.cos((5 * Math.PI) / 6), 0.00001); expect(res.y).to.be.closeTo(0, 0.00001); expect(res.z).to.be.closeTo(Math.sin((5 * Math.PI) / 6), 0.00001); @@ -1539,7 +1621,7 @@ suite('p5.Vector', function () { var res, angle; beforeEach(function () { angle = Math.PI / 2; - res = mockP5.Vector.fromAngle(angle); + res = Vector.fromAngle(angle); }); test('should be a p5.Vector with values (0,1)', function () { @@ -1551,7 +1633,7 @@ suite('p5.Vector', function () { suite('p5.Vector.random2D()', function () { var res; beforeEach(function () { - res = mockP5.Vector.random2D(); + res = Vector.random2D(); }); test('should be a unit p5.Vector', function () { @@ -1562,7 +1644,7 @@ suite('p5.Vector', function () { suite('p5.Vector.random3D()', function () { var res; beforeEach(function () { - res = mockP5.Vector.random3D(); + res = Vector.random3D(); }); test('should be a unit p5.Vector', function () { expect(res.mag()).to.be.closeTo(1, 0.01); @@ -1571,7 +1653,7 @@ suite('p5.Vector', function () { suite('array', function () { beforeEach(function () { - v = new mockP5.Vector(1, 23, 4); + v = new Vector(1, 23, 4); }); suite('p5.Vector.prototype.array() [INSTANCE]', function () { @@ -1586,11 +1668,11 @@ suite('p5.Vector', function () { suite('p5.Vector.array() [CLASS]', function () { test('should return an array', function () { - expect(mockP5.Vector.array(v)).to.be.instanceof(Array); + expect(Vector.array(v)).to.be.instanceof(Array); }); test('should return an with the x y and z components', function () { - expect(mockP5.Vector.array(v)).to.eql([1, 23, 4]); + expect(Vector.array(v)).to.eql([1, 23, 4]); }); }); }); @@ -1609,31 +1691,31 @@ suite('p5.Vector', function () { incoming_x = 1; incoming_y = 1; incoming_z = 1; - original_incoming = new mockP5.Vector( + original_incoming = new Vector( incoming_x, incoming_y, incoming_z ); - x_normal = new mockP5.Vector(3, 0, 0); - y_normal = new mockP5.Vector(0, 3, 0); - z_normal = new mockP5.Vector(0, 0, 3); + x_normal = new Vector(3, 0, 0); + y_normal = new Vector(0, 3, 0); + z_normal = new Vector(0, 0, 3); - x_bounce_incoming = new mockP5.Vector( + x_bounce_incoming = new Vector( incoming_x, incoming_y, incoming_z ); x_bounce_outgoing = x_bounce_incoming.reflect(x_normal); - y_bounce_incoming = new mockP5.Vector( + y_bounce_incoming = new Vector( incoming_x, incoming_y, incoming_z ); y_bounce_outgoing = y_bounce_incoming.reflect(y_normal); - z_bounce_incoming = new mockP5.Vector( + z_bounce_incoming = new Vector( incoming_x, incoming_y, incoming_z @@ -1642,9 +1724,9 @@ suite('p5.Vector', function () { }); test('should return a p5.Vector', function () { - expect(x_bounce_incoming).to.be.an.instanceof(mockP5.Vector); - expect(y_bounce_incoming).to.be.an.instanceof(mockP5.Vector); - expect(z_bounce_incoming).to.be.an.instanceof(mockP5.Vector); + expect(x_bounce_incoming).to.be.an.instanceof(Vector); + expect(y_bounce_incoming).to.be.an.instanceof(Vector); + expect(z_bounce_incoming).to.be.an.instanceof(Vector); }); test('should update this', function () { @@ -1720,47 +1802,47 @@ suite('p5.Vector', function () { incoming_x = 1; incoming_y = 1; incoming_z = 1; - original_incoming = new mockP5.Vector( + original_incoming = new Vector( incoming_x, incoming_y, incoming_z ); - x_target = new mockP5.Vector(); - y_target = new mockP5.Vector(); - z_target = new mockP5.Vector(); + x_target = new Vector(); + y_target = new Vector(); + z_target = new Vector(); - x_normal = new mockP5.Vector(3, 0, 0); - y_normal = new mockP5.Vector(0, 3, 0); - z_normal = new mockP5.Vector(0, 0, 3); + x_normal = new Vector(3, 0, 0); + y_normal = new Vector(0, 3, 0); + z_normal = new Vector(0, 0, 3); - x_bounce_incoming = new mockP5.Vector( + x_bounce_incoming = new Vector( incoming_x, incoming_y, incoming_z ); - x_bounce_outgoing = mockP5.Vector.reflect( + x_bounce_outgoing = Vector.reflect( x_bounce_incoming, x_normal, x_target ); - y_bounce_incoming = new mockP5.Vector( + y_bounce_incoming = new Vector( incoming_x, incoming_y, incoming_z ); - y_bounce_outgoing = mockP5.Vector.reflect( + y_bounce_outgoing = Vector.reflect( y_bounce_incoming, y_normal, y_target ); - z_bounce_incoming = new mockP5.Vector( + z_bounce_incoming = new Vector( incoming_x, incoming_y, incoming_z ); - z_bounce_outgoing = mockP5.Vector.reflect( + z_bounce_outgoing = Vector.reflect( z_bounce_incoming, z_normal, z_target @@ -1768,9 +1850,9 @@ suite('p5.Vector', function () { }); test('should return a p5.Vector', function () { - expect(x_bounce_incoming).to.be.an.instanceof(mockP5.Vector); - expect(y_bounce_incoming).to.be.an.instanceof(mockP5.Vector); - expect(z_bounce_incoming).to.be.an.instanceof(mockP5.Vector); + expect(x_bounce_incoming).to.be.an.instanceof(Vector); + expect(y_bounce_incoming).to.be.an.instanceof(Vector); + expect(z_bounce_incoming).to.be.an.instanceof(Vector); }); test('should not update this', function () { @@ -1846,8 +1928,8 @@ suite('p5.Vector', function () { let v1; beforeEach(function () { - v0 = new mockP5.Vector(0, 0, 0); - v1 = new mockP5.Vector(1, 2, 3); + v0 = new Vector(0, 0, 0); + v1 = new Vector(1, 2, 3); }); suite('p5.Vector.prototype.mag() [INSTANCE]', function () { @@ -1859,8 +1941,8 @@ suite('p5.Vector', function () { suite('p5.Vector.mag() [CLASS]', function () { test('should return the magnitude of the vector', function () { - expect(mockP5.Vector.mag(v0)).to.eql(0); - expect(mockP5.Vector.mag(v1)).to.eql(MAG); + expect(Vector.mag(v0)).to.eql(0); + expect(Vector.mag(v1)).to.eql(MAG); }); }); }); @@ -1872,8 +1954,8 @@ suite('p5.Vector', function () { let v1; beforeEach(function () { - v0 = new mockP5.Vector(0, 0, 0); - v1 = new mockP5.Vector(1, 2, 3); + v0 = new Vector(0, 0, 0); + v1 = new Vector(1, 2, 3); }); suite('p5.Vector.prototype.magSq() [INSTANCE]', function () { @@ -1885,8 +1967,8 @@ suite('p5.Vector', function () { suite('p5.Vector.magSq() [CLASS]', function () { test('should return the magnitude of the vector', function () { - expect(mockP5.Vector.magSq(v0)).to.eql(0); - expect(mockP5.Vector.magSq(v1)).to.eql(MAG); + expect(Vector.magSq(v0)).to.eql(0); + expect(Vector.magSq(v1)).to.eql(MAG); }); }); }); @@ -1894,8 +1976,8 @@ suite('p5.Vector', function () { suite('equals', function () { suite('p5.Vector.prototype.equals() [INSTANCE]', function () { test('should return false for parameters inequal to the vector', function () { - const v1 = new mockP5.Vector(0, -1, 1); - const v2 = new mockP5.Vector(1, 2, 3); + const v1 = new Vector(0, -1, 1); + const v2 = new Vector(1, 2, 3); const a2 = [1, 2, 3]; expect(v1.equals(v2)).to.be.false; expect(v1.equals(a2)).to.be.false; @@ -1903,57 +1985,57 @@ suite('p5.Vector', function () { }); test('should return true for equal vectors', function () { - const v1 = new mockP5.Vector(0, -1, 1); - const v2 = new mockP5.Vector(0, -1, 1); + const v1 = new Vector(0, -1, 1); + const v2 = new Vector(0, -1, 1); expect(v1.equals(v2)).to.be.true; }); test('should return true for arrays equal to the vector', function () { - const v1 = new mockP5.Vector(0, -1, 1); + const v1 = new Vector(0, -1, 1); const a1 = [0, -1, 1]; expect(v1.equals(a1)).to.be.true; }); test('should return true for arguments equal to the vector', function () { - const v1 = new mockP5.Vector(0, -1, 1); + const v1 = new Vector(0, -1, 1); expect(v1.equals(0, -1, 1)).to.be.true; }); }); suite('p5.Vector.equals() [CLASS]', function () { test('should return false for inequal parameters', function () { - const v1 = new mockP5.Vector(0, -1, 1); - const v2 = new mockP5.Vector(1, 2, 3); + const v1 = new Vector(0, -1, 1); + const v2 = new Vector(1, 2, 3); const a2 = [1, 2, 3]; - expect(mockP5.Vector.equals(v1, v2)).to.be.false; - expect(mockP5.Vector.equals(v1, a2)).to.be.false; - expect(mockP5.Vector.equals(a2, v1)).to.be.false; + expect(Vector.equals(v1, v2)).to.be.false; + expect(Vector.equals(v1, a2)).to.be.false; + expect(Vector.equals(a2, v1)).to.be.false; }); test('should return true for equal vectors', function () { - const v1 = new mockP5.Vector(0, -1, 1); - const v2 = new mockP5.Vector(0, -1, 1); - expect(mockP5.Vector.equals(v1, v2)).to.be.true; + const v1 = new Vector(0, -1, 1); + const v2 = new Vector(0, -1, 1); + expect(Vector.equals(v1, v2)).to.be.true; }); test('should return true for equal vectors and arrays', function () { - const v1 = new mockP5.Vector(0, -1, 1); + const v1 = new Vector(0, -1, 1); const a1 = [0, -1, 1]; - expect(mockP5.Vector.equals(v1, a1)).to.be.true; - expect(mockP5.Vector.equals(a1, v1)).to.be.true; + expect(Vector.equals(v1, a1)).to.be.true; + expect(Vector.equals(a1, v1)).to.be.true; }); test('should return true for equal arrays', function () { const a1 = [0, -1, 1]; const a2 = [0, -1, 1]; - expect(mockP5.Vector.equals(a1, a2)).to.be.true; + expect(Vector.equals(a1, a2)).to.be.true; }); }); }); suite('set values', function () { beforeEach(function () { - v = new mockP5.Vector(); + v = new Vector(); }); test('should NOT set values to [0,0,0] if values array is empty', function () { @@ -1966,7 +2048,7 @@ suite('p5.Vector', function () { }); suite('get value', function () { test('should return element in range of a non empty vector', function () { - let vect = new mockP5.Vector(1, 2, 3, 4); + let vect = new Vector(1, 2, 3, 4); assert.equal(vect.getValue(0), 1); assert.equal(vect.getValue(1), 2); assert.equal(vect.getValue(2), 3); @@ -1976,7 +2058,7 @@ suite('p5.Vector', function () { test.fails( 'should throw friendly error if attempting to get element outside lenght', function () { - let vect = new mockP5.Vector(1, 2, 3, 4); + let vect = new Vector(1, 2, 3, 4); assert.equal(vect.getValue(5), 1); } ); @@ -1984,7 +2066,7 @@ suite('p5.Vector', function () { suite('set value', function () { test('should set value of element in range', function () { - let vect = new mockP5.Vector(1, 2, 3, 4); + let vect = new Vector(1, 2, 3, 4); vect.setValue(0, 7); assert.equal(vect.getValue(0), 7); assert.equal(vect.getValue(1), 2); @@ -1995,7 +2077,7 @@ suite('p5.Vector', function () { test.fails( 'should throw friendly error if attempting to set element outside lenght', function () { - let vect = new mockP5.Vector(1, 2, 3, 4); + let vect = new Vector(1, 2, 3, 4); vect.setValue(100, 7); } ); @@ -2003,19 +2085,19 @@ suite('p5.Vector', function () { describe('get w', () => { it('should return the w component of the vector', () => { - v = new mockP5.Vector(1, 2, 3, 4); + v = new Vector(1, 2, 3, 4); expect(v.w).toBe(4); }); it('should return 0 if w component is not set', () => { - v = new mockP5.Vector(1, 2, 3); + v = new Vector(1, 2, 3); expect(v.w).toBe(0); }); }); describe('set w', () => { it('should set 4th dimension of vector to w value if it exists', () => { - v = new mockP5.Vector(1, 2, 3, 4); + v = new Vector(1, 2, 3, 4); v.w = 7; expect(v.x).toBe(1); expect(v.y).toBe(2); @@ -2024,7 +2106,7 @@ suite('p5.Vector', function () { }); it('should throw error if trying to set w if vector dimensions is less than 4', () => { - v = new mockP5.Vector(1, 2); + v = new Vector(1, 2); v.w = 5; console.log(v); console.log(v.w); @@ -2034,14 +2116,14 @@ suite('p5.Vector', function () { describe('vector to string', () => { it('should return the string version of a vector', () => { - v = new mockP5.Vector(1, 2, 3, 4); + v = new Vector(1, 2, 3, 4); expect(v.toString()).toBe('vector[1, 2, 3, 4]'); }); }); describe('set heading', () => { it('should rotate a 2D vector by specified angle without changing magnitude', () => { - v = new mockP5.Vector(0, 2); + v = new Vector(0, 2); const mag = v.mag(); expect(v.setHeading(2 * Math.PI).mag()).toBe(mag); expect(v.x).toBe(2); @@ -2051,23 +2133,23 @@ suite('p5.Vector', function () { describe('clamp to zero', () => { it('should clamp values cloze to zero to zero, with Number.epsilon value', () => { - v = new mockP5.Vector(0, 1, 0.5, 0.1, 0.0000000000000001); + v = new Vector(0, 1, 0.5, 0.1, 0.0000000000000001); expect(v.clampToZero().values).toEqual([0, 1, 0.5, 0.1, 0]); }); }); suite('p5.Vector.fromAngles()', function () { - it('should create a v3ctor froma pair of ISO spherical angles', () => { - let vect = mockP5.Vector.fromAngles(0, 0); + it('should create a vector froma pair of ISO spherical angles', () => { + let vect = Vector.fromAngles(0, 0); expect(vect.values).toEqual([0, -1, 0]); }); }); suite('p5.Vector.rotate()', function () { it('should rotate the vector (only 2D vectors) by the given angle; magnitude remains the same.', () => { - v = new mockP5.Vector(0, 1, 2); - let target = new mockP5.Vector(); - mockP5.Vector.rotate(v, 1 * Math.PI, target); + v = new Vector(0, 1, 2); + let target = new Vector(); + Vector.rotate(v, 1 * Math.PI, target); expect(target.values).toEqual([ -4.10759023698152e-16, -2.23606797749979, 2 ]); From 346a2acbacb2f5dcfc93ffc8a18b5ef2402ad0b1 Mon Sep 17 00:00:00 2001 From: ksen0 Date: Wed, 25 Feb 2026 15:34:15 +0100 Subject: [PATCH 036/250] Added benchmark tests --- test/bench/vectors.bench.js | 171 ++++++++++++++++++++++++++++++++++++ test/unit/math/p5.Vector.js | 7 +- 2 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 test/bench/vectors.bench.js diff --git a/test/bench/vectors.bench.js b/test/bench/vectors.bench.js new file mode 100644 index 0000000000..d8595be7c2 --- /dev/null +++ b/test/bench/vectors.bench.js @@ -0,0 +1,171 @@ +import {default as vector, Vector} from '../../src/math/p5.Vector.js'; + +import { bench, describe, beforeAll } from "vitest"; + +// This is not parameterizable because it's run all the time +function setupVectors() { + const n = 100; + const arr = [new Array(n)] + for (let i = 0; i < n; i++) arr[i] = new Vector(Math.random(), Math.random(), Math.random()) + return arr +} + + + +describe("vector operations", () => { + beforeAll(function () { + vector(mockP5Prototype); + }); + + + bench( + "mult 5", + () => { + const nLimited = 5; + const arr = setupVectors(); + + for (let i = 0; i < nLimited; i++) { + for (let j = 0; j < nLimited; j++) { + const tmp = arr[i].mult(arr[j]); + } + } + }, + { iterations: 100 } + ); + + bench( + "mult 10", + () => { + const nLimited = 10; + const arr = setupVectors(); + + for (let i = 0; i < nLimited; i++) { + for (let j = 0; j < nLimited; j++) { + const tmp = arr[i].mult(arr[j]); + } + } + }, + { iterations: 100 } + ); + + bench( + "mult 20", + () => { + const nLimited = 20; + const arr = setupVectors(); + + for (let i = 0; i < nLimited; i++) { + for (let j = 0; j < nLimited; j++) { + const tmp = arr[i].mult(arr[j]); + } + } + }, + { iterations: 100 } + ); + bench( + "mult 100", + () => { + const nLimited = 100; + const arr = setupVectors(); + + for (let i = 0; i < nLimited; i++) { + for (let j = 0; j < nLimited; j++) { + const tmp = arr[i].mult(arr[j]); + } + } + }, + { iterations: 100 } + ); + + + + bench( + "add 5", + () => { + const nLimited = 5; + const arr = setupVectors(); + + for (let i = 0; i < nLimited; i++) { + for (let j = 0; j < nLimited; j++) { + const tmp = arr[i].add(arr[j]); + } + } + }, + { iterations: 100 } + ); + + bench( + "add 10", + () => { + const nLimited = 10; + const arr = setupVectors(); + + for (let i = 0; i < nLimited; i++) { + for (let j = 0; j < nLimited; j++) { + const tmp = arr[i].add(arr[j]); + } + } + }, + { iterations: 100 } + ); + + bench( + "add 20", + () => { + const nLimited = 20; + const arr = setupVectors(); + + for (let i = 0; i < nLimited; i++) { + for (let j = 0; j < nLimited; j++) { + const tmp = arr[i].add(arr[j]); + } + } + }, + { iterations: 100 } + ); + bench( + "add 100", + () => { + const nLimited = 100; + const arr = setupVectors(); + + for (let i = 0; i < nLimited; i++) { + for (let j = 0; j < nLimited; j++) { + const tmp = arr[i].add(arr[j]); + } + } + }, + { iterations: 100 } + ); + + +/** + * possible in vitest 3? + function benchMult(nLimited) { + bench( + `mult ${nLimited}`, + (arr) => { + console.log(">",arr) + console.log(">",arr.length) + for (let i = 0; i < nLimited; i++) { + for (let j = 0; j < nLimited; j++) { + const tmp = arr[i].mult(arr[j]); + } + } + }, + { + iterations: 20, + setup: () => setupVectors(10000), + } + ) + } + + benchMult(10) + //benchMult(1000) + //benchMult(2000) + //benchMult(3000) + //benchMult(4000) + + */ + +}); diff --git a/test/unit/math/p5.Vector.js b/test/unit/math/p5.Vector.js index a7b45039d3..e5630c2b10 100644 --- a/test/unit/math/p5.Vector.js +++ b/test/unit/math/p5.Vector.js @@ -10,6 +10,8 @@ suite('p5.Vector', function () { beforeEach(async function () { vector(mockP5Prototype); + //console.log(typeof mockP5Prototype.createVector); // Debug this + //console.log(Object.keys(mockP5Prototype)); // See what was added }); afterEach(function () {}); @@ -84,6 +86,7 @@ suite('p5.Vector', function () { }); test('should have x, y, z be initialized to 0,0,0', function () { + console.log(typeof mockP5Prototype.createVector); assert.equal(v.x, 0); assert.equal(v.y, 0); assert.equal(v.z, 0); @@ -796,7 +799,7 @@ suite('p5.Vector', function () { }); }); - suite('p5.Vector.mult(v, arr', function () { + suite('p5.Vector.mult(v, arr)', function () { var v0, arr, res; beforeEach(function () { v0 = new Vector(1, 2, 3); @@ -852,7 +855,7 @@ suite('p5.Vector', function () { }); suite('with arglist', function () { - test('multiply the x, y, z with the scalar', function () { + test('divide the x, y, z with the scalar', function () { v.div(2, 3, 4); expect(v.x).to.be.closeTo(0.5, 0.01); expect(v.y).to.be.closeTo(0.333, 0.01); From 4837e8f3581f254b8fe701211b8a869a8460cf2d Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Tue, 3 Mar 2026 12:39:00 +0000 Subject: [PATCH 037/250] Try to set up pkg.pr.new --- .github/workflows/continuous-release.yml | 147 ++++++ package-lock.json | 592 ++++++++++++++++++++++- package.json | 1 + 3 files changed, 737 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/continuous-release.yml diff --git a/.github/workflows/continuous-release.yml b/.github/workflows/continuous-release.yml new file mode 100644 index 0000000000..c8ea3651cb --- /dev/null +++ b/.github/workflows/continuous-release.yml @@ -0,0 +1,147 @@ +name: Publish approved pull requests and latest commit to pkg.pr.new +on: + pull_request: + types: + - opened + branches: + - 'dev-2.0' + push: + branches: + - 'dev-2.0' + tags: + - '!**' + +permissions: {} + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install dependencies + run: npm ci + + - name: Build library + run: npm run build + + - name: Publish library + run: npx pkg-pr-new publish --no-template --json output.json --comment=off + + - name: Post or update comment + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); + + const packages = output.packages + .map((p) => `- ${p.name}: ${p.url}`) + .join('\n'); + + const cdnLinks = output.packages + .map((p) => `- ${p.name}: ${p.url.replace('pkg.pr.new', 'raw.esm.sh/pr')}/lib/p5.min.js`) + .join('\n'); + + const sha = + context.event_name === 'pull_request' + ? context.payload.pull_request.head.sha + : context.payload.after; + + const commitUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${sha}`; + + const body = `## Continuous Release + + ### CDN link + + ${cdnLinks} + + ### Published Packages: + + ${packages} + + [View Commit](${commitUrl}) + + --- + + _This is an automated message._`; + + const botCommentIdentifier = '## Continuous Release'; + + async function findBotComment(issueNumber) { + if (!issueNumber) return null; + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + }); + return comments.data.find((comment) => + comment.body.includes(botCommentIdentifier) + ); + } + + async function createOrUpdateComment(issueNumber) { + if (!issueNumber) { + console.log('No issue number provided. Cannot post or update comment.'); + return; + } + + const existingComment = await findBotComment(issueNumber); + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: body, + }); + } else { + await github.rest.issues.createComment({ + issue_number: issueNumber, + owner: context.repo.owner, + repo: context.repo.repo, + body: body, + }); + } + } + + async function logPublishInfo() { + console.log('\n' + '='.repeat(50)); + console.log('Publish Information'); + console.log('='.repeat(50)); + console.log('\nPublished Packages:'); + console.log(packages); + console.log('\nTemplates:'); + console.log(templates); + console.log(`\nCommit URL: ${commitUrl}`); + console.log('\n' + '='.repeat(50)); + } + + if (context.eventName === 'pull_request') { + if (context.issue.number) { + await createOrUpdateComment(context.issue.number); + } + } else if (context.eventName === 'push') { + const pullRequests = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${context.repo.owner}:${context.ref.replace( + 'refs/heads/', + '' + )}`, + }); + + if (pullRequests.data.length > 0) { + await createOrUpdateComment(pullRequests.data[0].number); + } else { + console.log( + 'No open pull request found for this push. Logging publish information to console:' + ); + await logPublishInfo(); + } + } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4334dbb370..3012154463 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "lint-staged": "^15.1.0", "msw": "^2.6.3", "pixelmatch": "^7.1.0", + "pkg-pr-new": "^0.0.65", "rollup": "^4.9.6", "rollup-plugin-string": "^3.0.0", "rollup-plugin-visualizer": "^5.12.0", @@ -56,6 +57,45 @@ "webdriverio": "^9.0.7" } }, + "node_modules/@actions/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", + "integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/exec": "^3.0.0", + "@actions/http-client": "^4.0.0" + } + }, + "node_modules/@actions/exec": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz", + "integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/io": "^3.0.2" + } + }, + "node_modules/@actions/http-client": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz", + "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^6.23.0" + } + }, + "node_modules/@actions/io": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz", + "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==", + "dev": true, + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -2273,6 +2313,22 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ez-spawn": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@jsdevtools/ez-spawn/-/ez-spawn-3.0.4.tgz", + "integrity": "sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-me-maybe": "^1.0.1", + "cross-spawn": "^7.0.3", + "string-argv": "^0.3.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@mswjs/interceptors": { "version": "0.37.6", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.6.tgz", @@ -2329,6 +2385,278 @@ "node": ">= 8" } }, + "node_modules/@octokit/action": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/action/-/action-6.1.0.tgz", + "integrity": "sha512-lo+nHx8kAV86bxvOVOI3vFjX3gXPd/L7guAUbvs3pUvnR2KC+R7yjBkA1uACt4gYhs4LcWP3AXSGQzsbeN2XXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-action": "^4.0.0", + "@octokit/core": "^5.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-rest-endpoint-methods": "^10.0.0", + "@octokit/types": "^12.0.0", + "undici": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-action": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-action/-/auth-action-4.1.0.tgz", + "integrity": "sha512-m+3t7K46IYyMk7Bl6/lF4Rv09GqDZjYmNg8IWycJ2Fa3YE3DE7vQcV6G2hUPmR9NDqenefNJwVtlisMjzymPiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/types": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-action/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/auth-action/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/endpoint/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/graphql/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/request-error/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/request/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, "node_modules/@open-draft/deferred-promise": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", @@ -4333,6 +4661,13 @@ "node": ">=10.0.0" } }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4452,6 +4787,13 @@ "dev": true, "license": "MIT" }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4949,6 +5291,13 @@ "node": ">=12" } }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -5165,6 +5514,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/decode-uri-component": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", + "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5217,6 +5576,13 @@ "node": ">= 14" } }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true, + "license": "ISC" + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -6353,6 +6719,19 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", + "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -7529,6 +7908,19 @@ "dev": true, "license": "MIT" }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -9915,6 +10307,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -10697,6 +11102,38 @@ "node": ">=10" } }, + "node_modules/pkg-pr-new": { + "version": "0.0.65", + "resolved": "https://registry.npmjs.org/pkg-pr-new/-/pkg-pr-new-0.0.65.tgz", + "integrity": "sha512-yZGN17qf6SX/go2rlLL3OWtwO0ppKZBxAfG5M3a9N44WMDrhqkuyfQOpKdvRE9plFsCzF7L/Xtp/1hQQHqhhCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/core": "^3.0.0", + "@jsdevtools/ez-spawn": "^3.0.4", + "@octokit/action": "^6.1.0", + "ignore": "^5.3.1", + "isbinaryfile": "^5.0.2", + "pkg-types": "^1.1.1", + "query-registry": "^3.0.1", + "tinyglobby": "^0.2.9" + }, + "bin": { + "pkg-pr-new": "bin/cli.js" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/please-upgrade-node": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", @@ -10871,6 +11308,34 @@ "node": ">=6" } }, + "node_modules/query-registry": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/query-registry/-/query-registry-3.0.1.tgz", + "integrity": "sha512-M9RxRITi2mHMVPU5zysNjctUT8bAPx6ltEXo/ir9+qmiM47Y7f0Ir3+OxUO5OjYAWdicBQRew7RtHtqUXydqlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "query-string": "^9.0.0", + "quick-lru": "^7.0.0", + "url-join": "^5.0.0", + "validate-npm-package-name": "^5.0.1", + "zod": "^3.23.8", + "zod-package-json": "^1.0.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/query-registry/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/query-selector-shadow-dom": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", @@ -10878,6 +11343,24 @@ "dev": true, "license": "MIT" }, + "node_modules/query-string": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.3.1.tgz", + "integrity": "sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.4.1", + "filter-obj": "^5.1.0", + "split-on-first": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -10906,6 +11389,19 @@ ], "license": "MIT" }, + "node_modules/quick-lru": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.3.0.tgz", + "integrity": "sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -11952,6 +12448,19 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/split-on-first": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", + "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -12391,6 +12900,16 @@ "dev": true, "license": "0BSD" }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -12404,6 +12923,16 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", @@ -12431,6 +12960,13 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, "node_modules/unbzip2-stream": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", @@ -12478,9 +13014,9 @@ } }, "node_modules/undici": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz", - "integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", "dev": true, "license": "MIT", "engines": { @@ -12612,6 +13148,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true, + "license": "ISC" + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -12663,6 +13206,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -12728,6 +13281,16 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/vfile": { "version": "5.3.7", "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", @@ -13565,6 +14128,29 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-package-json": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/zod-package-json/-/zod-package-json-1.2.0.tgz", + "integrity": "sha512-tamtgPM3MkP+obfO2dLr/G+nYoYkpJKmuHdYEy6IXRKfLybruoJ5NUj0lM0LxwOpC9PpoGLbll1ecoeyj43Wsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "zod": "^3.25.64" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/zod-package-json/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 345fd156ca..254bd68ba3 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "lint-staged": "^15.1.0", "msw": "^2.6.3", "pixelmatch": "^7.1.0", + "pkg-pr-new": "^0.0.65", "rollup": "^4.9.6", "rollup-plugin-string": "^3.0.0", "rollup-plugin-visualizer": "^5.12.0", From b5a2fcdd19e3d8581df8f2e3f01006bb9ced0e85 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Tue, 3 Mar 2026 12:42:57 +0000 Subject: [PATCH 038/250] Only run CR in processing repo --- .github/workflows/continuous-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/continuous-release.yml b/.github/workflows/continuous-release.yml index c8ea3651cb..0df3711a42 100644 --- a/.github/workflows/continuous-release.yml +++ b/.github/workflows/continuous-release.yml @@ -15,6 +15,7 @@ permissions: {} jobs: publish: + if: github.repository == 'processing/p5.js' runs-on: ubuntu-latest steps: - name: Checkout code From d4a966bf854f01b027c8ae097049d29e631497a2 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Tue, 3 Mar 2026 13:17:23 +0000 Subject: [PATCH 039/250] Provide PR write permission and set URL --- .github/workflows/continuous-release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-release.yml b/.github/workflows/continuous-release.yml index 0df3711a42..bcc62bc1de 100644 --- a/.github/workflows/continuous-release.yml +++ b/.github/workflows/continuous-release.yml @@ -17,6 +17,8 @@ jobs: publish: if: github.repository == 'processing/p5.js' runs-on: ubuntu-latest + permissions: + pull_requests: write steps: - name: Checkout code uses: actions/checkout@v4 @@ -46,7 +48,7 @@ jobs: .join('\n'); const cdnLinks = output.packages - .map((p) => `- ${p.name}: ${p.url.replace('pkg.pr.new', 'raw.esm.sh/pr')}/lib/p5.min.js`) + .map((p) => `- ${p.name}: https://raw.esm.sh/pr/p5@${p.sha}/lib/p5.min.js`) .join('\n'); const sha = From 0b893fea1446eb533f979fd0eb505f0e29d977aa Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Tue, 3 Mar 2026 13:20:18 +0000 Subject: [PATCH 040/250] Tying to get CI to run on PR push --- .github/workflows/continuous-release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/continuous-release.yml b/.github/workflows/continuous-release.yml index bcc62bc1de..b39e616470 100644 --- a/.github/workflows/continuous-release.yml +++ b/.github/workflows/continuous-release.yml @@ -1,8 +1,6 @@ name: Publish approved pull requests and latest commit to pkg.pr.new on: pull_request: - types: - - opened branches: - 'dev-2.0' push: From 8fdac7192ee464a58db5e6e660d6ecbbe7965520 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Tue, 3 Mar 2026 13:22:17 +0000 Subject: [PATCH 041/250] Another fix on CR running and commenting --- .github/workflows/continuous-release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-release.yml b/.github/workflows/continuous-release.yml index b39e616470..18036143bf 100644 --- a/.github/workflows/continuous-release.yml +++ b/.github/workflows/continuous-release.yml @@ -1,6 +1,8 @@ name: Publish approved pull requests and latest commit to pkg.pr.new on: pull_request: + types: + - opened branches: - 'dev-2.0' push: @@ -16,7 +18,7 @@ jobs: if: github.repository == 'processing/p5.js' runs-on: ubuntu-latest permissions: - pull_requests: write + issues: write steps: - name: Checkout code uses: actions/checkout@v4 From da46ad6d89cd902f10e724159233fc46ddb9ccd7 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Tue, 3 Mar 2026 13:25:02 +0000 Subject: [PATCH 042/250] We will squash merge this --- .github/workflows/continuous-release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/continuous-release.yml b/.github/workflows/continuous-release.yml index 18036143bf..152659224d 100644 --- a/.github/workflows/continuous-release.yml +++ b/.github/workflows/continuous-release.yml @@ -1,8 +1,6 @@ name: Publish approved pull requests and latest commit to pkg.pr.new on: pull_request: - types: - - opened branches: - 'dev-2.0' push: From 8a72ea46546efcd646407d7a8f16424cbb359325 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Tue, 3 Mar 2026 13:33:41 +0000 Subject: [PATCH 043/250] This may be the correct key --- .github/workflows/continuous-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-release.yml b/.github/workflows/continuous-release.yml index 152659224d..de737f801c 100644 --- a/.github/workflows/continuous-release.yml +++ b/.github/workflows/continuous-release.yml @@ -16,7 +16,7 @@ jobs: if: github.repository == 'processing/p5.js' runs-on: ubuntu-latest permissions: - issues: write + pull-requests: write steps: - name: Checkout code uses: actions/checkout@v4 From 28394b663213eed4a3d1400d03162be913dd03da Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Tue, 3 Mar 2026 13:41:15 +0000 Subject: [PATCH 044/250] Possibly this --- .github/workflows/continuous-release.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous-release.yml b/.github/workflows/continuous-release.yml index de737f801c..f6c91e7d9d 100644 --- a/.github/workflows/continuous-release.yml +++ b/.github/workflows/continuous-release.yml @@ -9,14 +9,13 @@ on: tags: - '!**' -permissions: {} +permissions: + pull-requests: write jobs: publish: if: github.repository == 'processing/p5.js' runs-on: ubuntu-latest - permissions: - pull-requests: write steps: - name: Checkout code uses: actions/checkout@v4 From 779fe764a3cfba3f346fd8c0d43d97c2ff0c3c22 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Tue, 3 Mar 2026 13:45:10 +0000 Subject: [PATCH 045/250] Another attempt --- .github/workflows/continuous-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/continuous-release.yml b/.github/workflows/continuous-release.yml index f6c91e7d9d..d27931a200 100644 --- a/.github/workflows/continuous-release.yml +++ b/.github/workflows/continuous-release.yml @@ -11,6 +11,7 @@ on: permissions: pull-requests: write + issues: write jobs: publish: From 9ba1e9f402c89f07d5cb53810833402b474282d5 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 4 Mar 2026 12:27:14 +0000 Subject: [PATCH 046/250] Use account based access token --- .github/workflows/continuous-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-release.yml b/.github/workflows/continuous-release.yml index d27931a200..1934f7c8f4 100644 --- a/.github/workflows/continuous-release.yml +++ b/.github/workflows/continuous-release.yml @@ -36,7 +36,7 @@ jobs: - name: Post or update comment uses: actions/github-script@v8 with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.ACCESS_TOKEN }} script: | const fs = require('fs'); const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); From e5df5f96082da9605d869c8338b2ad446828a539 Mon Sep 17 00:00:00 2001 From: ksen0 Date: Wed, 4 Mar 2026 16:19:21 +0100 Subject: [PATCH 047/250] Updating tests for vectors - unit and bench --- lib/empty-example/sketch.js | 90 ++++++++++++++++++++++++++++++++++--- src/math/math.js | 1 + src/math/p5.Vector.js | 6 +-- src/math/patch-vector.js | 23 +++++++++- test/bench/vectors.bench.js | 73 +++++------------------------- test/unit/math/p5.Vector.js | 10 ++--- 6 files changed, 126 insertions(+), 77 deletions(-) diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index 336fa0777f..a10ee85f04 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -1,7 +1,85 @@ function setup() { - // put setup code here - } - - function draw() { - // put drawing code here - } + createCanvas(200, 200); + background(0, 100, 100); + noLoop(); +} + +function draw() { + +let v0, v1, v2, v3; + v0 = new p5.Vector(); + v1 = new p5.Vector([1]); + v2 = new p5.Vector([2, 3]); + v3 = new p5.Vector([4, 5, 6]); + + print('should be prioritized in add()') + v0 = v1.add(v2) + console.log(v0.x, v0.y, v0.z) + console.log(v1.add(v2).values, [2]); + console.log(v1.add(v2).dimensions, 1); + console.log(v3.add(v2).values, [8,15]); + console.log(v3.add(v2).dimensions, 2); + + print('should be prioritized in sub()') + console.log(v1.sub(v2).values, [-1]); + console.log(v1.sub(v2).dimensions, 1); + console.log(v3.sub(v2).values, [2, 2]); + console.log(v3.sub(v2).dimensions, 2); + print('should be prioritized in mult()') + console.log(v1.mult(v2).values, [2]); + console.log(v1.mult(v2).dimensions, 1); + console.log(v3.mult(v2).values, [8, 15]); + console.log(v3.mult(v2).dimensions, 2); + + print('should be prioritized in div()'); + console.log(v1.div(v2).values, [1/2]); + console.log(v1.div(v2).dimensions, 1); + console.log(v3.div(v2).values, [2, 5/3]); + console.log(v3.div(v2).dimensions, 2); + + print('should be prioritized in rem()') + console.log(v1.rem(v2).values, [1]); + console.log(v1.rem(v2).dimensions, 1); + console.log(v3.rem(v2).values, [0, 2]); + console.log(v3.rem(v2).dimensions, 2); + + return; + + + const arr_random = []; + for (let i = 0; i < 100+2; i++) { + arr_random.push(random()); + } + + const arr = []; + + // TODO: + // for(let iters = 100; iters < 5000; i+= 100) + + console.log("creating vecotrs") + console.time(); + for (let i = 0; i < 100; i++) { + arr.push(createVector(arr_random[i], arr_random[i+1], arr_random[i+2])); + } + + console.timeEnd(); + + + console.log("pairwise multiplying them vecotrs") + console.time(); + + + for (let i = 0; i < arr.length; i++) { + for (let j = 0; j < arr.length; j++) { + const _ = arr[i].mult(arr[j].mult(2)); + } + } + + console.timeEnd(); + + //default: 1.7999999523162842ms + // default: 2ms + //default: 0.20000004768371582ms but I got a "wait or kill" dialog + + background(100, 0, 100); +} diff --git a/src/math/math.js b/src/math/math.js index f965963cfa..11c7aed170 100644 --- a/src/math/math.js +++ b/src/math/math.js @@ -93,6 +93,7 @@ function math(p5, fn) { * } */ fn.createVector = function (x, y, z) { + // TODO if (this instanceof p5) { return new p5.Vector( this._fromRadians.bind(this), diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 51248984fd..9382549fb9 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -15,8 +15,8 @@ const smallerDimensionPriority = function(dimOther, dimSelf) { 'When working with two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as ' + minDimension + 'D vectors, and any additional values of the linger vector will be ignored.', ); } - return minDimension -} + return minDimension; +}; class Vector { /** @@ -51,7 +51,7 @@ class Vector { console.warn( 'In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector.' ); - args = [0, 0, 0] + args = [0, 0, 0]; } this.values = args; diff --git a/src/math/patch-vector.js b/src/math/patch-vector.js index 6929daae73..8ac609334a 100644 --- a/src/math/patch-vector.js +++ b/src/math/patch-vector.js @@ -1,8 +1,10 @@ export default function patchVector(p5, fn, lifecycles){ // An empty vector defaults to a 3D vector. - p5.decorateHelper('createVector', function(target){ + // TODO might need p5.prototype + p5.registerDecorator('createVector', function(target){ return function(...args){ + console.log("!!!!!") if(args.length === 0){ p5._friendlyError( 'In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector.' @@ -14,4 +16,23 @@ export default function patchVector(p5, fn, lifecycles){ }; }); + p5.registerDecorator('p5.Vector.prototype.add', function(target){ + return function(...args){ + console.log("hi") + }; + }); + /* + add(...args) { + + // TODO Implement using decorator API to reduce duplication. + if (args[0] instanceof Vector) { + args = args[0].values; + } else if (Array.isArray(args[0])) { + args = args[0]; + } else if (args.length === 0) { + return this; + } + + */ + } diff --git a/test/bench/vectors.bench.js b/test/bench/vectors.bench.js index d8595be7c2..952c428509 100644 --- a/test/bench/vectors.bench.js +++ b/test/bench/vectors.bench.js @@ -1,36 +1,23 @@ -import {default as vector, Vector} from '../../src/math/p5.Vector.js'; - -import { bench, describe, beforeAll } from "vitest"; - -// This is not parameterizable because it's run all the time -function setupVectors() { - const n = 100; - const arr = [new Array(n)] - for (let i = 0; i < n; i++) arr[i] = new Vector(Math.random(), Math.random(), Math.random()) - return arr -} +import { Vector } from '../../src/math/p5.Vector.js'; +import { bench, describe } from "vitest"; describe("vector operations", () => { - beforeAll(function () { - vector(mockP5Prototype); - }); - bench( "mult 5", () => { const nLimited = 5; - const arr = setupVectors(); + // TODO try just operating on det. values based on i and j + // without re-creation for (let i = 0; i < nLimited; i++) { for (let j = 0; j < nLimited; j++) { const tmp = arr[i].mult(arr[j]); } } - }, - { iterations: 100 } + } ); bench( @@ -44,8 +31,7 @@ describe("vector operations", () => { const tmp = arr[i].mult(arr[j]); } } - }, - { iterations: 100 } + } ); bench( @@ -59,8 +45,7 @@ describe("vector operations", () => { const tmp = arr[i].mult(arr[j]); } } - }, - { iterations: 100 } + } ); bench( "mult 100", @@ -73,8 +58,7 @@ describe("vector operations", () => { const tmp = arr[i].mult(arr[j]); } } - }, - { iterations: 100 } + } ); @@ -90,8 +74,7 @@ describe("vector operations", () => { const tmp = arr[i].add(arr[j]); } } - }, - { iterations: 100 } + } ); bench( @@ -105,9 +88,7 @@ describe("vector operations", () => { const tmp = arr[i].add(arr[j]); } } - }, - { iterations: 100 } - ); + }); bench( "add 20", @@ -120,8 +101,7 @@ describe("vector operations", () => { const tmp = arr[i].add(arr[j]); } } - }, - { iterations: 100 } + } ); bench( "add 100", @@ -135,37 +115,6 @@ describe("vector operations", () => { } } }, - { iterations: 100 } ); - - -/** - * possible in vitest 3? - function benchMult(nLimited) { - bench( - `mult ${nLimited}`, - (arr) => { - console.log(">",arr) - console.log(">",arr.length) - for (let i = 0; i < nLimited; i++) { - for (let j = 0; j < nLimited; j++) { - const tmp = arr[i].mult(arr[j]); - } - } - }, - { - iterations: 20, - setup: () => setupVectors(10000), - } - ) - } - - benchMult(10) - //benchMult(1000) - //benchMult(2000) - //benchMult(3000) - //benchMult(4000) - - */ }); diff --git a/test/unit/math/p5.Vector.js b/test/unit/math/p5.Vector.js index e5630c2b10..63673cf02c 100644 --- a/test/unit/math/p5.Vector.js +++ b/test/unit/math/p5.Vector.js @@ -1,4 +1,5 @@ -import {default as vector, Vector} from '../../../src/math/p5.Vector.js'; +import {Vector} from '../../../src/math/p5.Vector.js'; +import {default as math} from '../../../src/math/math.js'; import { vi } from 'vitest'; // TODO add create Vector coverage @@ -6,12 +7,11 @@ import { vi } from 'vitest'; suite('p5.Vector', function () { var v; + const mockP5 = {}; const mockP5Prototype = {}; - beforeEach(async function () { - vector(mockP5Prototype); - //console.log(typeof mockP5Prototype.createVector); // Debug this - //console.log(Object.keys(mockP5Prototype)); // See what was added + beforeAll(async function () { + math(mockP5, mockP5Prototype); }); afterEach(function () {}); From be704cc227beb23fdaf3d47282560f6503392fec Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 4 Mar 2026 21:11:24 +0000 Subject: [PATCH 048/250] Switch to uploading artifact and Github App --- .github/workflows/continuous-release.yml | 230 ++++++++++++----------- 1 file changed, 118 insertions(+), 112 deletions(-) diff --git a/.github/workflows/continuous-release.yml b/.github/workflows/continuous-release.yml index 1934f7c8f4..45c33809c5 100644 --- a/.github/workflows/continuous-release.yml +++ b/.github/workflows/continuous-release.yml @@ -33,116 +33,122 @@ jobs: - name: Publish library run: npx pkg-pr-new publish --no-template --json output.json --comment=off - - name: Post or update comment - uses: actions/github-script@v8 + - name: Upload output data + uses: actions/upload-artifact@v4 with: - github-token: ${{ secrets.ACCESS_TOKEN }} - script: | - const fs = require('fs'); - const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); - - const packages = output.packages - .map((p) => `- ${p.name}: ${p.url}`) - .join('\n'); - - const cdnLinks = output.packages - .map((p) => `- ${p.name}: https://raw.esm.sh/pr/p5@${p.sha}/lib/p5.min.js`) - .join('\n'); - - const sha = - context.event_name === 'pull_request' - ? context.payload.pull_request.head.sha - : context.payload.after; - - const commitUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${sha}`; - - const body = `## Continuous Release - - ### CDN link - - ${cdnLinks} - - ### Published Packages: - - ${packages} - - [View Commit](${commitUrl}) - - --- - - _This is an automated message._`; - - const botCommentIdentifier = '## Continuous Release'; - - async function findBotComment(issueNumber) { - if (!issueNumber) return null; - const comments = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - }); - return comments.data.find((comment) => - comment.body.includes(botCommentIdentifier) - ); - } - - async function createOrUpdateComment(issueNumber) { - if (!issueNumber) { - console.log('No issue number provided. Cannot post or update comment.'); - return; - } - - const existingComment = await findBotComment(issueNumber); - if (existingComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existingComment.id, - body: body, - }); - } else { - await github.rest.issues.createComment({ - issue_number: issueNumber, - owner: context.repo.owner, - repo: context.repo.repo, - body: body, - }); - } - } - - async function logPublishInfo() { - console.log('\n' + '='.repeat(50)); - console.log('Publish Information'); - console.log('='.repeat(50)); - console.log('\nPublished Packages:'); - console.log(packages); - console.log('\nTemplates:'); - console.log(templates); - console.log(`\nCommit URL: ${commitUrl}`); - console.log('\n' + '='.repeat(50)); - } - - if (context.eventName === 'pull_request') { - if (context.issue.number) { - await createOrUpdateComment(context.issue.number); - } - } else if (context.eventName === 'push') { - const pullRequests = await github.rest.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - head: `${context.repo.owner}:${context.ref.replace( - 'refs/heads/', - '' - )}`, - }); - - if (pullRequests.data.length > 0) { - await createOrUpdateComment(pullRequests.data[0].number); - } else { - console.log( - 'No open pull request found for this push. Logging publish information to console:' - ); - await logPublishInfo(); - } - } \ No newline at end of file + name: output.json + path: output.json + + # - name: Post or update comment + # uses: actions/github-script@v8 + # with: + # github-token: ${{ secrets.ACCESS_TOKEN }} + # script: | + # const fs = require('fs'); + # const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); + + # const packages = output.packages + # .map((p) => `- ${p.name}: ${p.url}`) + # .join('\n'); + + # const cdnLinks = output.packages + # .map((p) => `- ${p.name}: https://raw.esm.sh/pr/p5@${p.sha}/lib/p5.min.js`) + # .join('\n'); + + # const sha = + # context.event_name === 'pull_request' + # ? context.payload.pull_request.head.sha + # : context.payload.after; + + # const commitUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${sha}`; + + # const body = `## Continuous Release + + # ### CDN link + + # ${cdnLinks} + + # ### Published Packages: + + # ${packages} + + # [View Commit](${commitUrl}) + + # --- + + # _This is an automated message._`; + + # const botCommentIdentifier = '## Continuous Release'; + + # async function findBotComment(issueNumber) { + # if (!issueNumber) return null; + # const comments = await github.rest.issues.listComments({ + # owner: context.repo.owner, + # repo: context.repo.repo, + # issue_number: issueNumber, + # }); + # return comments.data.find((comment) => + # comment.body.includes(botCommentIdentifier) + # ); + # } + + # async function createOrUpdateComment(issueNumber) { + # if (!issueNumber) { + # console.log('No issue number provided. Cannot post or update comment.'); + # return; + # } + + # const existingComment = await findBotComment(issueNumber); + # if (existingComment) { + # await github.rest.issues.updateComment({ + # owner: context.repo.owner, + # repo: context.repo.repo, + # comment_id: existingComment.id, + # body: body, + # }); + # } else { + # await github.rest.issues.createComment({ + # issue_number: issueNumber, + # owner: context.repo.owner, + # repo: context.repo.repo, + # body: body, + # }); + # } + # } + + # async function logPublishInfo() { + # console.log('\n' + '='.repeat(50)); + # console.log('Publish Information'); + # console.log('='.repeat(50)); + # console.log('\nPublished Packages:'); + # console.log(packages); + # console.log('\nTemplates:'); + # console.log(templates); + # console.log(`\nCommit URL: ${commitUrl}`); + # console.log('\n' + '='.repeat(50)); + # } + + # if (context.eventName === 'pull_request') { + # if (context.issue.number) { + # await createOrUpdateComment(context.issue.number); + # } + # } else if (context.eventName === 'push') { + # const pullRequests = await github.rest.pulls.list({ + # owner: context.repo.owner, + # repo: context.repo.repo, + # state: 'open', + # head: `${context.repo.owner}:${context.ref.replace( + # 'refs/heads/', + # '' + # )}`, + # }); + + # if (pullRequests.data.length > 0) { + # await createOrUpdateComment(pullRequests.data[0].number); + # } else { + # console.log( + # 'No open pull request found for this push. Logging publish information to console:' + # ); + # await logPublishInfo(); + # } + # } \ No newline at end of file From 28ae294cd8067694e6f209bb721ea7df3cea8fc6 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Thu, 5 Mar 2026 11:40:29 +0000 Subject: [PATCH 049/250] Include PR number in output.json --- .github/workflows/continuous-release.yml | 131 +++-------------------- 1 file changed, 16 insertions(+), 115 deletions(-) diff --git a/.github/workflows/continuous-release.yml b/.github/workflows/continuous-release.yml index 45c33809c5..7409e879c4 100644 --- a/.github/workflows/continuous-release.yml +++ b/.github/workflows/continuous-release.yml @@ -33,122 +33,23 @@ jobs: - name: Publish library run: npx pkg-pr-new publish --no-template --json output.json --comment=off + - name: Include PR info in output file + uses: actions/github-script@v8 + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + with: + script: | + const fs = require('fs'); + const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); + output.workflow = { + pull_request: process.env.PR_NUMBER ? { + number: process.env.PR_NUMBER + } : null + }; + fs.writeFileSync('output.json', JSON.stringify(output)); + - name: Upload output data uses: actions/upload-artifact@v4 with: name: output.json - path: output.json - - # - name: Post or update comment - # uses: actions/github-script@v8 - # with: - # github-token: ${{ secrets.ACCESS_TOKEN }} - # script: | - # const fs = require('fs'); - # const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); - - # const packages = output.packages - # .map((p) => `- ${p.name}: ${p.url}`) - # .join('\n'); - - # const cdnLinks = output.packages - # .map((p) => `- ${p.name}: https://raw.esm.sh/pr/p5@${p.sha}/lib/p5.min.js`) - # .join('\n'); - - # const sha = - # context.event_name === 'pull_request' - # ? context.payload.pull_request.head.sha - # : context.payload.after; - - # const commitUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${sha}`; - - # const body = `## Continuous Release - - # ### CDN link - - # ${cdnLinks} - - # ### Published Packages: - - # ${packages} - - # [View Commit](${commitUrl}) - - # --- - - # _This is an automated message._`; - - # const botCommentIdentifier = '## Continuous Release'; - - # async function findBotComment(issueNumber) { - # if (!issueNumber) return null; - # const comments = await github.rest.issues.listComments({ - # owner: context.repo.owner, - # repo: context.repo.repo, - # issue_number: issueNumber, - # }); - # return comments.data.find((comment) => - # comment.body.includes(botCommentIdentifier) - # ); - # } - - # async function createOrUpdateComment(issueNumber) { - # if (!issueNumber) { - # console.log('No issue number provided. Cannot post or update comment.'); - # return; - # } - - # const existingComment = await findBotComment(issueNumber); - # if (existingComment) { - # await github.rest.issues.updateComment({ - # owner: context.repo.owner, - # repo: context.repo.repo, - # comment_id: existingComment.id, - # body: body, - # }); - # } else { - # await github.rest.issues.createComment({ - # issue_number: issueNumber, - # owner: context.repo.owner, - # repo: context.repo.repo, - # body: body, - # }); - # } - # } - - # async function logPublishInfo() { - # console.log('\n' + '='.repeat(50)); - # console.log('Publish Information'); - # console.log('='.repeat(50)); - # console.log('\nPublished Packages:'); - # console.log(packages); - # console.log('\nTemplates:'); - # console.log(templates); - # console.log(`\nCommit URL: ${commitUrl}`); - # console.log('\n' + '='.repeat(50)); - # } - - # if (context.eventName === 'pull_request') { - # if (context.issue.number) { - # await createOrUpdateComment(context.issue.number); - # } - # } else if (context.eventName === 'push') { - # const pullRequests = await github.rest.pulls.list({ - # owner: context.repo.owner, - # repo: context.repo.repo, - # state: 'open', - # head: `${context.repo.owner}:${context.ref.replace( - # 'refs/heads/', - # '' - # )}`, - # }); - - # if (pullRequests.data.length > 0) { - # await createOrUpdateComment(pullRequests.data[0].number); - # } else { - # console.log( - # 'No open pull request found for this push. Logging publish information to console:' - # ); - # await logPublishInfo(); - # } - # } \ No newline at end of file + path: output.json \ No newline at end of file From 01aa360790fed6e43b1cbec18f6bbefc5a4210a0 Mon Sep 17 00:00:00 2001 From: VANSH3104 Date: Fri, 6 Mar 2026 10:44:48 +0530 Subject: [PATCH 050/250] Add ShapePrimitive support for arcs and ellipses --- src/core/p5.Renderer2D.js | 60 ++------------- src/shape/custom_shapes.js | 151 +++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 52 deletions(-) diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index fbe5747449..206d187f5f 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -8,7 +8,7 @@ import { MediaElement } from '../dom/p5.MediaElement'; import { RGBHDR } from '../color/creating_reading'; import FilterRenderer2D from '../image/filterRenderer2D'; import { Matrix } from '../math/p5.Matrix'; -import { PrimitiveToPath2DConverter } from '../shape/custom_shapes'; +import { PrimitiveToPath2DConverter, ArcPrimitive, EllipsePrimitive } from '../shape/custom_shapes'; import { DefaultFill, textCoreConstants } from '../type/textCore'; @@ -661,13 +661,6 @@ class Renderer2D extends Renderer { * start <= stop < start + TWO_PI */ arc(x, y, w, h, start, stop, mode) { - const ctx = this.drawingContext; - const rx = w / 2.0; - const ry = h / 2.0; - const epsilon = 0.00001; // Smallest visible angle on displays up to 4K. - let arcToDraw = 0; - const curves = []; - const centerX = x + w / 2, centerY = y + h / 2, radiusX = w / 2, @@ -681,48 +674,16 @@ class Renderer2D extends Renderer { 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 === constants.CHORD || - mode === constants.OPEN || - (stop - start) % constants.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 === constants.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 === constants.PIE || mode === constants.CHORD) { - // Stroke connects back to path begin for both PIE and CHORD - ctx.closePath(); - } - ctx.stroke(); - } + const primitive = new ArcPrimitive(x, y, w, h, start, stop, mode); + const shape = { accept(visitor) { primitive.accept(visitor); } }; + this.drawShape(shape); return this; } ellipse(args) { - const ctx = this.drawingContext; const doFill = !!this.states.fillColor, doStroke = this.states.strokeColor; const x = parseFloat(args[0]), @@ -751,15 +712,10 @@ class Renderer2D extends Renderer { 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(); - } + + const primitive = new EllipsePrimitive(x, y, w, h); + const shape = { accept(visitor) { primitive.accept(visitor); } }; + this.drawShape(shape); return this; } diff --git a/src/shape/custom_shapes.js b/src/shape/custom_shapes.js index 3a09200f75..61ffe2ba4c 100644 --- a/src/shape/custom_shapes.js +++ b/src/shape/custom_shapes.js @@ -466,6 +466,77 @@ class Quad extends ShapePrimitive { } } +class ArcPrimitive extends ShapePrimitive { + #x; + #y; + #w; + #h; + #start; + #stop; + #mode; + // vertexCapacity 0 means this primitive should not accumulate normal path vertices + #vertexCapacity = 0; + + constructor(x, y, w, h, start, stop, mode) { + // ShapePrimitive requires at least one vertex; pass a placeholder + super(new Vertex({ position: new Vector(x + w / 2, y + h / 2) })); + this.#x = x; + this.#y = y; + this.#w = w; + this.#h = h; + this.#start = start; + this.#stop = stop; + this.#mode = mode; + } + + get x() { return this.#x; } + get y() { return this.#y; } + get w() { return this.#w; } + get h() { return this.#h; } + get start() { return this.#start; } + get stop() { return this.#stop; } + get mode() { return this.#mode; } + + get vertexCapacity() { + return this.#vertexCapacity; + } + + accept(visitor) { + visitor.visitArcPrimitive(this); + } +} + +class EllipsePrimitive extends ShapePrimitive { + #x; + #y; + #w; + #h; + // vertexCapacity 0 means this primitive should not accumulate normal path vertices + #vertexCapacity = 0; + + constructor(x, y, w, h) { + + super(new Vertex({ position: new Vector(x + w / 2, y + h / 2) })); + this.#x = x; + this.#y = y; + this.#w = w; + this.#h = h; + } + + get x() { return this.#x; } + get y() { return this.#y; } + get w() { return this.#w; } + get h() { return this.#h; } + + get vertexCapacity() { + return this.#vertexCapacity; + } + + accept(visitor) { + visitor.visitEllipsePrimitive(this); + } +} + // ---- TESSELLATION PRIMITIVES ---- class TriangleFan extends ShapePrimitive { @@ -1003,6 +1074,12 @@ class PrimitiveVisitor { visitArcSegment(arcSegment) { throw new Error('Method visitArcSegment() has not been implemented.'); } + visitArcPrimitive(arc) { + throw new Error('Method visitArcPrimitive() has not been implemented.'); + } + visitEllipsePrimitive(ellipse) { + throw new Error('Method visitEllipsePrimitive() has not been implemented.'); + } // isolated primitives visitPoint(point) { @@ -1151,6 +1228,34 @@ class PrimitiveToPath2DConverter extends PrimitiveVisitor { this.path.closePath(); } } + visitArcPrimitive(arc) { + const centerX = arc.x + arc.w / 2; + const centerY = arc.y + arc.h / 2; + const radiusX = arc.w / 2; + const radiusY = arc.h / 2; + + this.path.ellipse( + centerX, centerY, radiusX, radiusY, 0, arc.start, arc.stop + ); + + if (arc.mode === constants.OPEN) { + // OPEN: leave path open — arc stroke/fill is just the curve + } else if (arc.mode === constants.CHORD) { + + this.path.closePath(); + } else { + this.path.lineTo(centerX, centerY); + this.path.closePath(); + } + } + visitEllipsePrimitive(ellipse) { + const centerX = ellipse.x + ellipse.w / 2; + const centerY = ellipse.y + ellipse.h / 2; + const radiusX = ellipse.w / 2; + const radiusY = ellipse.h / 2; + + this.path.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); + } visitQuadStrip(quadStrip) { for (let i = 0; i < quadStrip.vertices.length - 3; i += 2) { const v0 = quadStrip.vertices[i]; @@ -1277,6 +1382,50 @@ class PrimitiveToVerticesConverter extends PrimitiveVisitor { // WebGL itself interprets the vertices as a strip, no reformatting needed this.contours.push(quadStrip.vertices.slice()); } + visitArcPrimitive(arc) { + const centerX = arc.x + arc.w / 2; + const centerY = arc.y + arc.h / 2; + const radiusX = arc.w / 2; + const radiusY = arc.h / 2; + const numPoints = Math.max(3, this.curveDetail); + const verts = []; + + if (arc.mode === constants.PIE) { + verts.push(new Vertex({ position: new Vector(centerX, centerY) })); + } + + for (let i = 0; i <= numPoints; i++) { + const angle = arc.start + (arc.stop - arc.start) * (i / numPoints); + verts.push(new Vertex({ + position: new Vector( + centerX + radiusX * Math.cos(angle), + centerY + radiusY * Math.sin(angle) + ) + })); + } + + this.contours.push(verts); + } + visitEllipsePrimitive(ellipse) { + const centerX = ellipse.x + ellipse.w / 2; + const centerY = ellipse.y + ellipse.h / 2; + const radiusX = ellipse.w / 2; + const radiusY = ellipse.h / 2; + const numPoints = Math.max(3, this.curveDetail); + const verts = []; + + for (let i = 0; i <= numPoints; i++) { + const angle = (2 * Math.PI * i) / numPoints; + verts.push(new Vertex({ + position: new Vector( + centerX + radiusX * Math.cos(angle), + centerY + radiusY * Math.sin(angle) + ) + })); + } + + this.contours.push(verts); + } } class PointAtLengthGetter extends PrimitiveVisitor { @@ -2793,6 +2942,8 @@ export { Line, Triangle, Quad, + ArcPrimitive, + EllipsePrimitive, TriangleFan, TriangleStrip, QuadStrip, From da8a3365c47a33fe9db4ff00e5cc3e53a99449eb Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Fri, 6 Mar 2026 10:25:18 +0000 Subject: [PATCH 051/250] Rename output artifact file --- .github/workflows/continuous-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-release.yml b/.github/workflows/continuous-release.yml index 7409e879c4..44114d0f49 100644 --- a/.github/workflows/continuous-release.yml +++ b/.github/workflows/continuous-release.yml @@ -51,5 +51,5 @@ jobs: - name: Upload output data uses: actions/upload-artifact@v4 with: - name: output.json + name: output.zip path: output.json \ No newline at end of file From 613df14708d0d8347a5ad576f0fbbbafa605ddbf Mon Sep 17 00:00:00 2001 From: ksen0 Date: Mon, 9 Mar 2026 11:11:46 +0100 Subject: [PATCH 052/250] Update vector documentation --- src/math/math.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/math/math.js b/src/math/math.js index 11c7aed170..e54a45196e 100644 --- a/src/math/math.js +++ b/src/math/math.js @@ -6,6 +6,9 @@ function math(p5, fn) { /** + * + * Testing + * * Creates a new p5.Vector object. * * A vector can be thought of in different ways. In one view, a vector is like @@ -93,16 +96,7 @@ function math(p5, fn) { * } */ fn.createVector = function (x, y, z) { - // TODO - if (this instanceof p5) { - return new p5.Vector( - this._fromRadians.bind(this), - this._toRadians.bind(this), - ...arguments - ); - } else { - return new p5.Vector(x, y, z); - } + return new p5.Vector(x, y, z); }; /** From 3e527ce2579ff5c319f08687aba08f43bf8d91d2 Mon Sep 17 00:00:00 2001 From: nbogie Date: Mon, 9 Mar 2026 16:12:40 +0000 Subject: [PATCH 053/250] add initial working_with_contributor_documents.md --- .../working_with_contributor_documents.md | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 contributor_docs/working_with_contributor_documents.md diff --git a/contributor_docs/working_with_contributor_documents.md b/contributor_docs/working_with_contributor_documents.md new file mode 100644 index 0000000000..d35f8acfb7 --- /dev/null +++ b/contributor_docs/working_with_contributor_documents.md @@ -0,0 +1,177 @@ + +# Working with contributor documents + +## Table of Contents +* [Where are they?](#where-are-they) +* [Build process overview](#build-process-overview) +* [Generating and previewing contributor documents](#generating-and-previewing-contributor-documents) +* [Adding a new contributor document](#adding-a-new-contributor-document) + +## Where are they? +Contributor documents are displayed on the p5.js website at either: + +* https://p5js.org/contribute/ (for p5.js v1) +* https://beta.p5js.org/contribute/ (for p5.js v2) + +Their source materials are kept in: +* repo: `p5.js` +* path: `contributor_docs/` + +(Note: The v1.x and v2.x branches have different documents.) + +## Build process overview + +During the website build process `build:contributor-docs`, the documents are cloned from the requested branch of the p5.js repo into the relevant website file-system locations. + +## Generating and previewing contributor documents + +### Quick preview +For a quick preview, various editors have a feature to render markdown files. For example, if you're using vs code here's how to do that: + +* open the .md file you wish to preview +* open the command-palette (`F1` or `cmd-shift-p` or `ctrl-shift-p`) +* type `Markdown: open preview` + + + +### Preview on local p5.js-website clone + +At some point you will want to preview how your changes will look on the website. This involves run the website locally and having it import the contributor docs from a branch of your p5.js repo. + +In the following steps we'll assume your p5.js repository is in a folder called `p5.js` and your p5.js-website repo is in a folder next to it called `p5.js-website`. + +```mermaid +--- +title: Assumed local repo folder setup +--- +flowchart TD + parent --- p5.js + parent --- p5.js-website +``` + +#### Steps: + +1. Commit your changes to a local branch of your fork of the p5.js repo. The changes don't need to be pushed to github for this purpose, but they do need to be committed on a branch. +1. Clone [the p5.js-website repo](https://github.com/processing/p5.js-website/tree/2.0) locally. +1. Open a terminal in your new p5.js-website repo +1. Check out the branch "2.0" +1. Run `npm install` +1. Modify and run the following command, using the path to **your** local p5.js repo, and the name of **your** branch: + +(Note the following is a single line, not two lines!) + +```sh +P5_REPO_URL=path/to/your/p5/repo P5_BRANCH=your-branch-goes-here npm run build:contributor-docs && npm run dev +``` + +For example, if your work is in a branch called `my-amazing-branch` on a local p5.js repo called `p5.js` as a sibling folder next to the current `p5.js-website` folder, you could run the following: + +```sh +P5_REPO_URL=../p5.js P5_BRANCH=my-amazing-branch npm run build:contributor-docs && npm run dev +``` + +This will do three things: +1. import and build local website `.mdx` pages from the `.md` files in `contributor_docs/` in your branch +2. start a development preview of the website +3. display a URL in the console where you can visit the local website + +Use your browser to visit the URL mentioned in the last step, in order to test out your changes. + +#### Alternative: Building from a branch on github + +If you prefer to preview work that's already on github, you can do so. In the final command, use the repo URL instead of its local path, as follows: + +(Again, note the following is a single line, not two lines!) + +```sh +P5_REPO_URL=https://github.com/yourUsername/p5.js.git P5_BRANCH=your-branch-goes-here npm run build:contributor-docs && npm run dev +``` + +#### Troubleshooting + +If your file isn't appearing in the list the website shows at the path `contribute/`: + +* Note that it will appear with a title taken from the first level 1 markdown heading, NOT the name of the file. + +* Check that a corresponding `.mdx` file is being generated in `src/content/contributor-docs/en` in the website file-structure. If not, check if your .md file is git in the branch you've specified, in the correct location, and check the logs, during the website build:contributor-docs + +* Review the log from the above run of the npm `build:contributor-docs` process, for mentions of your file(s). + +* If you see that an .mdx file _is_ being generated, check that you can access it directly on the website by typing its URL. e.g. if your file is called myFile.md, the path in the URL would be: `contribute/myFile/` + +* Don't forget that if you're [using local asset files](#using-assets), they'll need to be in the *website* repo, _not_ the main p5.js repo! + +* Ensure you see in the log that your repo _has_ actually been cloned. There is a caching mechanism in the website build process which prevents a recently cloned repo from being cloned again. Removing the website folder `in/p5.js/` will force the build process to make a new clone. + +#### Limitations + +The website won't be _fully_ functional when partially prepared in this way. Notably: + +* Links between pages may be broken: + * You'll need to ensure local links end with a trailing slash '/', to be matched by Astro when it is in development mode. +* The search facility will not work by default + * Look into `npm run build:search` to build the necessary index files. + + +## Adding a new contributor document + +We'll consider file path, filename, file extension, title, subtitle, and eventual URL path for your new document. + +#### The file path + +It should be stored in `contributor_docs/` folder in the _p5.js_ repo. + +It should be stored as a direct child of that folder, _not_ in a subfolder. + +#### The filename and extension + +The filename won't be used as the document title but _will_ be used in URLs. The filename should be all in lowercase, and use underscore characters `_` instead of spaces or dashes. + +It should have a `.md` file extension. + +Keep the filename concise but do not use contractions. e.g. "documentation_style_guide" not "doc_style_guide". If in doubt check the names of the other documents in the folder and try to stay aligned with those. + +#### The _name_ of your document +For presentation purposes in the list of contributor docs and for search results, your document title will be taken from the first level 1 markdown heading, NOT the name of the file. + +#### The subtitle for your document +In the list of contributor docs, each page is listed not only with its title but also a subtitle or short description. + +This description is extracted from the first HTML-style comment in your file. This should be on the first line, before the level 1 heading. + +Example: + + + +In `contributor_docs/unit_testing.md` the file content starts: + +
+<!‐- Guide to writing tests for p5.js source code. ‐-> + +\# Unit Testing +
+ +As a result, this will list a page titled "Unit Testing" with a description of "Guide to writing tests for p5.js source code.". + +#### The URL for your document + +The path in the eventual URL will be +`contribute/your-filename-without-extension/` +Note that the trailing slash is necessary in development mode. + +Example: + +The source document `contributor_docs/unit_testing.md` will be served as `https://beta.p5js.org/contribute/unit_testing/` + + + + From 80fbd60328820dcc2ff47930577cb27ed0a40285 Mon Sep 17 00:00:00 2001 From: nbogie Date: Tue, 10 Mar 2026 14:15:45 +0000 Subject: [PATCH 054/250] contrib docs guide: remove incorrect notes on assets --- contributor_docs/working_with_contributor_documents.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contributor_docs/working_with_contributor_documents.md b/contributor_docs/working_with_contributor_documents.md index d35f8acfb7..fbe58aa992 100644 --- a/contributor_docs/working_with_contributor_documents.md +++ b/contributor_docs/working_with_contributor_documents.md @@ -32,7 +32,7 @@ For a quick preview, various editors have a feature to render markdown files. F * open the command-palette (`F1` or `cmd-shift-p` or `ctrl-shift-p`) * type `Markdown: open preview` - + ### Preview on local p5.js-website clone @@ -99,8 +99,6 @@ If your file isn't appearing in the list the website shows at the path `contribu * If you see that an .mdx file _is_ being generated, check that you can access it directly on the website by typing its URL. e.g. if your file is called myFile.md, the path in the URL would be: `contribute/myFile/` -* Don't forget that if you're [using local asset files](#using-assets), they'll need to be in the *website* repo, _not_ the main p5.js repo! - * Ensure you see in the log that your repo _has_ actually been cloned. There is a caching mechanism in the website build process which prevents a recently cloned repo from being cloned again. Removing the website folder `in/p5.js/` will force the build process to make a new clone. #### Limitations From ce6a0485765a6727e41bbbeacc173bd905e7c70c Mon Sep 17 00:00:00 2001 From: kit Date: Thu, 12 Mar 2026 10:14:51 +0100 Subject: [PATCH 055/250] Operation logic in helpers --- lib/empty-example/sketch.js | 90 ++--------------------------- src/math/index.js | 4 +- src/math/math.js | 4 +- src/math/p5.Vector.js | 111 +++++------------------------------- src/math/patch-vector.js | 88 ++++++++++++++++++---------- test/unit/math/p5.Vector.js | 26 ++++++++- 6 files changed, 105 insertions(+), 218 deletions(-) diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index a10ee85f04..336fa0777f 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -1,85 +1,7 @@ function setup() { - createCanvas(200, 200); - background(0, 100, 100); - noLoop(); -} - -function draw() { - -let v0, v1, v2, v3; - v0 = new p5.Vector(); - v1 = new p5.Vector([1]); - v2 = new p5.Vector([2, 3]); - v3 = new p5.Vector([4, 5, 6]); - - print('should be prioritized in add()') - v0 = v1.add(v2) - console.log(v0.x, v0.y, v0.z) - console.log(v1.add(v2).values, [2]); - console.log(v1.add(v2).dimensions, 1); - console.log(v3.add(v2).values, [8,15]); - console.log(v3.add(v2).dimensions, 2); - - print('should be prioritized in sub()') - console.log(v1.sub(v2).values, [-1]); - console.log(v1.sub(v2).dimensions, 1); - console.log(v3.sub(v2).values, [2, 2]); - console.log(v3.sub(v2).dimensions, 2); - print('should be prioritized in mult()') - console.log(v1.mult(v2).values, [2]); - console.log(v1.mult(v2).dimensions, 1); - console.log(v3.mult(v2).values, [8, 15]); - console.log(v3.mult(v2).dimensions, 2); - - print('should be prioritized in div()'); - console.log(v1.div(v2).values, [1/2]); - console.log(v1.div(v2).dimensions, 1); - console.log(v3.div(v2).values, [2, 5/3]); - console.log(v3.div(v2).dimensions, 2); - - print('should be prioritized in rem()') - console.log(v1.rem(v2).values, [1]); - console.log(v1.rem(v2).dimensions, 1); - console.log(v3.rem(v2).values, [0, 2]); - console.log(v3.rem(v2).dimensions, 2); - - return; - - - const arr_random = []; - for (let i = 0; i < 100+2; i++) { - arr_random.push(random()); - } - - const arr = []; - - // TODO: - // for(let iters = 100; iters < 5000; i+= 100) - - console.log("creating vecotrs") - console.time(); - for (let i = 0; i < 100; i++) { - arr.push(createVector(arr_random[i], arr_random[i+1], arr_random[i+2])); - } - - console.timeEnd(); - - - console.log("pairwise multiplying them vecotrs") - console.time(); - - - for (let i = 0; i < arr.length; i++) { - for (let j = 0; j < arr.length; j++) { - const _ = arr[i].mult(arr[j].mult(2)); - } - } - - console.timeEnd(); - - //default: 1.7999999523162842ms - // default: 2ms - //default: 0.20000004768371582ms but I got a "wait or kill" dialog - - background(100, 0, 100); -} + // put setup code here + } + + function draw() { + // put drawing code here + } diff --git a/src/math/index.js b/src/math/index.js index 40d8d75924..8e08dd88bd 100644 --- a/src/math/index.js +++ b/src/math/index.js @@ -4,7 +4,7 @@ import random from './random.js'; import trigonometry from './trigonometry.js'; import math from './math.js'; import vector from './p5.Vector.js'; -import patchVector from './patch-vector.js'; +import vectorValidation from './patch-vector.js'; export default function(p5){ p5.registerAddon(calculation); @@ -13,5 +13,5 @@ export default function(p5){ p5.registerAddon(trigonometry); p5.registerAddon(math); p5.registerAddon(vector); - p5.registerAddon(patchVector); + p5.registerAddon(vectorValidation); } diff --git a/src/math/math.js b/src/math/math.js index e54a45196e..89c44956a1 100644 --- a/src/math/math.js +++ b/src/math/math.js @@ -95,8 +95,8 @@ function math(p5, fn) { * point(pos); * } */ - fn.createVector = function (x, y, z) { - return new p5.Vector(x, y, z); + fn.createVector = function (...args) { + return new p5.Vector(...args); }; /** diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 9382549fb9..f0b9899792 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -5,14 +5,15 @@ import * as constants from '../core/constants'; -/// HELPER FOR SMALLER DIMENSION PRIORITY LOGIC. -/// Pending implementation as decorator. +/** + * This function is used by binary vector operations to prioritize shorter vectors, + * and to emit a warning when lengths do not match. + */ const smallerDimensionPriority = function(dimOther, dimSelf) { const minDimension = Math.min(dimOther, dimSelf); if (dimOther !== dimSelf) { - console.warn( - 'When working with two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as ' + minDimension + 'D vectors, and any additional values of the linger vector will be ignored.', + `Operating on two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as ${minDimension}D vectors, and any additional values of the longer vector will be ignored.` ); } return minDimension; @@ -45,15 +46,6 @@ class Vector { args = args.slice(2); } - // TODO Implement using decorator API to reduce duplication. - // Should use the same check as patchVector on 'createVector' - if(args.length === 0){ - console.warn( - 'In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector.' - ); - args = [0, 0, 0]; - } - this.values = args; } @@ -470,17 +462,8 @@ class Vector { * @chainable */ add(...args) { - - // TODO Implement using decorator API to reduce duplication. - if (args[0] instanceof Vector) { - args = args[0].values; - } else if (Array.isArray(args[0])) { - args = args[0]; - } else if (args.length === 0) { - return this; - } - const minDimension = smallerDimensionPriority(args.length, this.dimensions); + this.values = this.values.reduce((acc, v, i) => { if(i < minDimension) acc[i] = this.values[i] + Number(args[i]); return acc; @@ -599,35 +582,11 @@ class Vector { * @chainable */ rem(...args) { - - // TODO Implement using decorator API to reduce duplication. - - if (args[0] instanceof Vector) { - args = args[0].values; - } else if (Array.isArray(args[0])) { - args = args[0]; - } else if (args.length === 1) { - args = new Array(this.dimensions).fill(args[0]); - } else if (args.length === 0) { - return this; - } - - if(!args.every(v => Number.isFinite(v))){ - console.warn( - 'p5.Vector.prototype.rem', - 'Arguments contain non-finite numbers' - ); - return this; - }; - const minDimension = smallerDimensionPriority(args.length, this.dimensions); - this.values = this.values.reduce((acc, v, i) => { - // Extra check for non empty operand - if(i < minDimension && args[i] > 0) acc[i] = this.values[i] % args[i]; - else acc[i] = this.values[i] - return acc; - }, new Array(minDimension)); + this.values = Array.from({ length: minDimension }, (_, i) => { + return (args[i] > 0) ? this.values[i] % args[i] : this.values[i]; + }); return this; } @@ -752,16 +711,6 @@ class Vector { * @chainable */ sub(...args) { - - // TODO Implement using decorator API to reduce duplication. - if (args[0] instanceof Vector) { - args = args[0].values; - } else if (Array.isArray(args[0])) { - args = args[0]; - } else if (args.length === 0) { - return this; - } - const minDimension = smallerDimensionPriority(args.length, this.dimensions); this.values = this.values.reduce((acc, v, i) => { @@ -947,26 +896,6 @@ class Vector { * @chainable */ mult(...args) { - // TODO Implement using decorator API to reduce duplication. - - if (args[0] instanceof Vector) { - args = args[0].values; - } else if (Array.isArray(args[0])) { - args = args[0]; - } else if (args.length === 1) { - args = new Array(this.dimensions).fill(args[0]); - } else if (args.length === 0) { - return this; - } - - if(!args.every(v => Number.isFinite(v))){ - console.warn( - 'p5.Vector.prototype.mult', - 'Arguments contain non-finite numbers' - ); - return this; - }; - const minDimension = smallerDimensionPriority(args.length, this.dimensions); this.values = this.values.reduce((acc, v, i) => { @@ -1191,30 +1120,16 @@ class Vector { * @chainable */ div(...args) { + const minDimension = smallerDimensionPriority(args.length, this.dimensions); - // TODO Implement using decorator API to reduce duplication. - - if (args[0] instanceof Vector) { - args = args[0].values; - } else if (Array.isArray(args[0])) { - args = args[0]; - } else if (args.length === 1) { - args = new Array(this.dimensions).fill(args[0]); - - } else if (args.length === 0) { - return this; - } - - if(!args.every(v => typeof v === 'number' && v !== 0 && Number.isFinite(v))){ + if(!args.every(v => typeof v === 'number' && v !== 0)){ console.warn( - 'p5.Vector.prototype.div:', - 'arguments contain components that are either 0 or not finite numbers' + 'p5.Vector.prototype.div', + 'Arguments contain components that are 0' ); return this; }; - const minDimension = smallerDimensionPriority(args.length, this.dimensions); - this.values = this.values.reduce((acc, v, i) => { if(i < minDimension) acc[i] = this.values[i] / args[i]; return acc; diff --git a/src/math/patch-vector.js b/src/math/patch-vector.js index 8ac609334a..075b0aa5e5 100644 --- a/src/math/patch-vector.js +++ b/src/math/patch-vector.js @@ -1,38 +1,68 @@ -export default function patchVector(p5, fn, lifecycles){ - - // An empty vector defaults to a 3D vector. - // TODO might need p5.prototype - p5.registerDecorator('createVector', function(target){ - return function(...args){ - console.log("!!!!!") - if(args.length === 0){ - p5._friendlyError( - 'In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector.' - ); - return target.call(this, 0, 0, 0); - }else{ - return target.call(this, ...args); - } - }; - }); +import { Vector } from './p5.Vector.js'; - p5.registerDecorator('p5.Vector.prototype.add', function(target){ - return function(...args){ - console.log("hi") - }; - }); - /* - add(...args) { +/** + * @private + * @internal + */ +export function _defaultEmptyVector(target){ + return function(...args){ + if(args.length === 0){ + p5._friendlyError( + 'In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector.', + 'p5.createVector' + ); + return target.call(this, 0, 0, 0); + }else{ + return target.call(this, ...args); + } + }; +} - // TODO Implement using decorator API to reduce duplication. - if (args[0] instanceof Vector) { + +/** + * @private + * @internal + */ +export function _validatedVectorOperation(target){ + return function(...args){ + if (args.length === 0) { + // No arguments? No action + return this; + } else if (args[0] instanceof Vector) { args = args[0].values; } else if (Array.isArray(args[0])) { args = args[0]; - } else if (args.length === 0) { - return this; + } else if (args.length === 1) { + // Solo argument? This is a special case + args = new Array(3).fill(args[0]); } - */ + if(!args.every(v => typeof v === 'number' && Number.isFinite(v))){ + p5._friendlyError( + 'Arguments contain non-finite numbers', + target.name + ); + return this; + }; + + return target.call(this, ...args); + }; +} + +/** + * Each of the following decorators validates the data on vector operations. + * These ensure that the arguments are consistently formatted, and that + * pre-conditions are met. + */ +export default function vectorValidation(p5, fn, lifecycles){ + + p5.registerDecorator('p5.prototype.createVector', _defaultEmptyVector); + + p5.registerDecorator('p5.Vector.prototype.add', _validatedVectorOperation); + p5.registerDecorator('p5.Vector.prototype.sub', _validatedVectorOperation); + p5.registerDecorator('p5.Vector.prototype.mult', _validatedVectorOperation); + + p5.registerDecorator('p5.Vector.prototype.rem', _validatedVectorOperation); + p5.registerDecorator('p5.Vector.prototype.div', _validatedVectorOperation); } diff --git a/test/unit/math/p5.Vector.js b/test/unit/math/p5.Vector.js index 63673cf02c..a28717be49 100644 --- a/test/unit/math/p5.Vector.js +++ b/test/unit/math/p5.Vector.js @@ -1,8 +1,8 @@ -import {Vector} from '../../../src/math/p5.Vector.js'; -import {default as math} from '../../../src/math/math.js'; +import { Vector } from '../../../src/math/p5.Vector.js'; +import { default as math } from '../../../src/math/math.js'; +import { _defaultEmptyVector, _validatedVectorOperation } from '../../../src/math/patch-vector.js'; import { vi } from 'vitest'; -// TODO add create Vector coverage suite('p5.Vector', function () { var v; @@ -11,7 +11,27 @@ suite('p5.Vector', function () { const mockP5Prototype = {}; beforeAll(async function () { + // Makes createVector available + mockP5.Vector = Vector; math(mockP5, mockP5Prototype); + + // Ensures all decorators are used by unit tests + mockP5Prototype.createVector = _defaultEmptyVector( + mockP5Prototype.createVector + ); + + // The following mocks simulate the validation decorator + Vector.prototype.add = _validatedVectorOperation(Vector.prototype.add); + Vector.prototype.sub = _validatedVectorOperation(Vector.prototype.sub); + Vector.prototype.mult = _validatedVectorOperation(Vector.prototype.mult); + Vector.prototype.rem = _validatedVectorOperation(Vector.prototype.rem); + Vector.prototype.div = _validatedVectorOperation(Vector.prototype.div); + + globalThis.p5 = { + _friendlyError: function(msg, func) { + console.warn(msg); + } + }; }); afterEach(function () {}); From e18443876eb22cd8ee7743c2e22996ce77c159da Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 12 Mar 2026 16:06:36 -0400 Subject: [PATCH 056/250] Ternary support for p5.strands --- src/strands/ir_types.js | 1 + src/strands/strands_api.js | 5 +++ src/strands/strands_ternary.js | 53 +++++++++++++++++++++++ src/strands/strands_transpiler.js | 14 +++++++ src/webgl/strands_glslBackend.js | 7 ++++ src/webgpu/strands_wgslBackend.js | 7 ++++ test/unit/webgl/p5.Shader.js | 70 +++++++++++++++++++++++++++++++ test/unit/webgpu/p5.Shader.js | 50 +++++++++++++++++++++- 8 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 src/strands/strands_ternary.js diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 9f480d5c9d..5347ba81d9 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -130,6 +130,7 @@ export const OpCode = { Nary: { FUNCTION_CALL: 200, CONSTRUCTOR: 201, + TERNARY: 202, }, ControlFlow: { RETURN: 300, diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index ef4c0424c3..6239e5a314 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -15,6 +15,7 @@ import { import { strandsBuiltinFunctions } from './strands_builtins' import { StrandsConditional } from './strands_conditionals' import { StrandsFor } from './strands_for' +import { buildTernary } from './strands_ternary' import * as CFG from './ir_cfg' import * as DAG from './ir_dag'; import * as FES from './strands_FES' @@ -194,6 +195,10 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return new StrandsFor(strandsContext, initialCb, conditionCb, updateCb, bodyCb, initialVars).build(); }; augmentFn(fn, p5, 'strandsFor', p5.strandsFor); + p5.strandsTernary = function(condition, ifTrue, ifFalse) { + return buildTernary(strandsContext, condition, ifTrue, ifFalse); + }; + augmentFn(fn, p5, 'strandsTernary', p5.strandsTernary); p5.strandsEarlyReturn = function(value) { const { dag, cfg } = strandsContext; diff --git a/src/strands/strands_ternary.js b/src/strands/strands_ternary.js new file mode 100644 index 0000000000..dcd84522ce --- /dev/null +++ b/src/strands/strands_ternary.js @@ -0,0 +1,53 @@ +import * as DAG from './ir_dag'; +import * as CFG from './ir_cfg'; +import { NodeType, OpCode, BaseType } from './ir_types'; +import { createStrandsNode } from './strands_node'; +import * as FES from './strands_FES'; + +export function buildTernary(strandsContext, condition, ifTrue, ifFalse) { + const { dag, cfg, p5 } = strandsContext; + + // Ensure all inputs are StrandsNodes + const condNode = condition?.isStrandsNode ? condition : p5.strandsNode(condition); + const trueNode = ifTrue?.isStrandsNode ? ifTrue : p5.strandsNode(ifTrue); + const falseNode = ifFalse?.isStrandsNode ? ifFalse : p5.strandsNode(ifFalse); + + // Get type info for both nodes + let trueType = DAG.extractNodeTypeInfo(dag, trueNode.id); + let falseType = DAG.extractNodeTypeInfo(dag, falseNode.id); + + // Propagate type from the known branch to any ASSIGN_ON_USE branch + if (trueType.baseType === BaseType.ASSIGN_ON_USE && falseType.baseType !== BaseType.ASSIGN_ON_USE) { + DAG.propagateTypeToAssignOnUse(dag, trueNode.id, falseType.baseType, falseType.dimension); + trueType = DAG.extractNodeTypeInfo(dag, trueNode.id); + } else if (falseType.baseType === BaseType.ASSIGN_ON_USE && trueType.baseType !== BaseType.ASSIGN_ON_USE) { + DAG.propagateTypeToAssignOnUse(dag, falseNode.id, trueType.baseType, trueType.dimension); + falseType = DAG.extractNodeTypeInfo(dag, falseNode.id); + } + + // After ASSIGN_ON_USE propagation, if both types are known, they must match + if ( + trueType.baseType !== BaseType.ASSIGN_ON_USE && + falseType.baseType !== BaseType.ASSIGN_ON_USE && + (trueType.baseType !== falseType.baseType || trueType.dimension !== falseType.dimension) + ) { + FES.userError('type error', + 'The true and false branches of a ternary expression must have the same type. ' + + `Right now, the true branch is a ${trueType.baseType}${trueType.dimension}, and the false branch is a ${falseType.baseType}${falseType.dimension}.` + ); + } + + const resultType = trueType; + + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Nary.TERNARY, + dependsOn: [condNode.id, trueNode.id, falseNode.id], + baseType: resultType.baseType, + dimension: resultType.dimension, + }); + + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return createStrandsNode(id, resultType.dimension, strandsContext); +} diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index 836a177c6c..df0e770795 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -465,6 +465,20 @@ const ASTCallbacks = { }; node.arguments = [node.right]; }, + ConditionalExpression(node, _state, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } + // Transform condition ? consequent : alternate + // into __p5.strandsTernary(condition, consequent, alternate) + const test = node.test; + const consequent = node.consequent; + const alternate = node.alternate; + node.type = 'CallExpression'; + node.callee = { type: 'Identifier', name: '__p5.strandsTernary' }; + node.arguments = [test, consequent, alternate]; + delete node.test; + delete node.consequent; + delete node.alternate; + }, IfStatement(node, _state, ancestors) { if (ancestors.some(nodeIsUniform)) { return; } // Transform if statement into strandsIf() call diff --git a/src/webgl/strands_glslBackend.js b/src/webgl/strands_glslBackend.js index daf804a8e8..1004487a66 100644 --- a/src/webgl/strands_glslBackend.js +++ b/src/webgl/strands_glslBackend.js @@ -289,6 +289,13 @@ export const glslBackend = { const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg)); return `${node.identifier}(${functionArgs.join(', ')})`; } + if (node.opCode === OpCode.Nary.TERNARY) { + const [condID, trueID, falseID] = node.dependsOn; + const cond = this.generateExpression(generationContext, dag, condID); + const trueExpr = this.generateExpression(generationContext, dag, trueID); + const falseExpr = this.generateExpression(generationContext, dag, falseID); + return `(${cond} ? ${trueExpr} : ${falseExpr})`; + } if (node.opCode === OpCode.Binary.MEMBER_ACCESS) { const [lID, rID] = node.dependsOn; const lName = this.generateExpression(generationContext, dag, lID); diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index 4394210414..81369446fc 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -396,6 +396,13 @@ export const wgslBackend = { const deps = node.dependsOn.map((dep) => this.generateExpression(generationContext, dag, dep)); return `${T}(${deps.join(', ')})`; } + if (node.opCode === OpCode.Nary.TERNARY) { + const [condID, trueID, falseID] = node.dependsOn; + const cond = this.generateExpression(generationContext, dag, condID); + const trueExpr = this.generateExpression(generationContext, dag, trueID); + const falseExpr = this.generateExpression(generationContext, dag, falseID); + return `select(${falseExpr}, ${trueExpr}, ${cond})`; + } if (node.opCode === OpCode.Nary.FUNCTION_CALL) { // Convert mod() function calls to % operator in WGSL if (node.identifier === 'mod' && node.dependsOn.length === 2) { diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 2556c1d25d..c2a16b21e7 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -1204,6 +1204,55 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca }); }); + suite('ternary expressions', () => { + test('ternary changes color based on left/right side of canvas', () => { + myp5.createCanvas(50, 25, myp5.WEBGL); + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + inputs.color = inputs.texCoord.x > 0.5 ? [1, 0, 0, 1] : [0, 0, 1, 1]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + const leftPixel = myp5.get(12, 12); + assert.approximately(leftPixel[0], 0, 5); + assert.approximately(leftPixel[1], 0, 5); + assert.approximately(leftPixel[2], 255, 5); + + const rightPixel = myp5.get(37, 12); + assert.approximately(rightPixel[0], 255, 5); + assert.approximately(rightPixel[1], 0, 5); + assert.approximately(rightPixel[2], 0, 5); + }); + + test('ternary with scalar values', () => { + myp5.createCanvas(50, 25, myp5.WEBGL); + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + const brightness = inputs.texCoord.x > 0.5 ? 1.0 : 0.0; + inputs.color = [brightness, brightness, brightness, 1]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + const leftPixel = myp5.get(12, 12); + assert.approximately(leftPixel[0], 0, 5); + assert.approximately(leftPixel[1], 0, 5); + assert.approximately(leftPixel[2], 0, 5); + + const rightPixel = myp5.get(37, 12); + assert.approximately(rightPixel[0], 255, 5); + assert.approximately(rightPixel[1], 255, 5); + assert.approximately(rightPixel[2], 255, 5); + }); + }); + suite('for loop statements', () => { test('handle simple for loop with known iteration count', () => { myp5.createCanvas(50, 50, myp5.WEBGL); @@ -2183,5 +2232,26 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca assert.include(errMsg, 'Expected properties'); assert.include(errMsg, 'Received properties'); }); + + test('ternary with mismatched branch types shows both types in error', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + try { + myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + // float1 vs float4 - type mismatch + const val = inputs.texCoord.x > 0.5 ? myp5.float(1.0) : [1, 0, 0, 1]; + inputs.color = [val, val, val, 1]; + return inputs; + }); + }, { myp5 }); + } catch (e) { /* expected */ } + + assert.isAbove(mockUserError.mock.calls.length, 0, 'FES.userError should have been called'); + const errMsg = mockUserError.mock.calls[0][1]; + assert.include(errMsg, 'ternary'); + assert.include(errMsg, 'float1'); + assert.include(errMsg, 'float4'); + }); }); }); diff --git a/test/unit/webgpu/p5.Shader.js b/test/unit/webgpu/p5.Shader.js index ba3bcd6bdc..7274ec84bf 100644 --- a/test/unit/webgpu/p5.Shader.js +++ b/test/unit/webgpu/p5.Shader.js @@ -490,7 +490,6 @@ suite('WebGPU p5.Shader', function() { return [0.4, 0, 0, 1]; }); }, { myp5 }); - console.log(testShader.fragSrc()) myp5.background(255, 255, 255); myp5.filter(testShader); @@ -502,6 +501,55 @@ suite('WebGPU p5.Shader', function() { }); }); + suite('ternary expressions', () => { + test('ternary changes color based on left/right side of canvas', async () => { + await myp5.createCanvas(50, 25, myp5.WEBGPU); + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + inputs.color = inputs.texCoord.x > 0.5 ? [1, 0, 0, 1] : [0, 0, 1, 1]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + const leftPixel = await myp5.get(12, 12); + assert.approximately(leftPixel[0], 0, 5); + assert.approximately(leftPixel[1], 0, 5); + assert.approximately(leftPixel[2], 255, 5); + + const rightPixel = await myp5.get(37, 12); + assert.approximately(rightPixel[0], 255, 5); + assert.approximately(rightPixel[1], 0, 5); + assert.approximately(rightPixel[2], 0, 5); + }); + + test('ternary with scalar values', async () => { + await myp5.createCanvas(50, 25, myp5.WEBGPU); + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + const brightness = inputs.texCoord.x > 0.5 ? 1.0 : 0.0; + inputs.color = [brightness, brightness, brightness, 1]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + const leftPixel = await myp5.get(12, 12); + assert.approximately(leftPixel[0], 0, 5); + assert.approximately(leftPixel[1], 0, 5); + assert.approximately(leftPixel[2], 0, 5); + + const rightPixel = await myp5.get(37, 12); + assert.approximately(rightPixel[0], 255, 5); + assert.approximately(rightPixel[1], 255, 5); + assert.approximately(rightPixel[2], 255, 5); + }); + }); + suite('for loop statements', () => { test('handle simple for loop with known iteration count', async () => { await myp5.createCanvas(50, 50, myp5.WEBGPU); From 1b6f33f352cb51540b4434057be83de4cec4e3bb Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 14 Mar 2026 11:04:14 -0400 Subject: [PATCH 057/250] Put back code that got lost in the merge --- src/strands/ir_builders.js | 3 --- src/strands/strands_transpiler.js | 24 +++++++++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index ef84fe6915..4e9c7eb700 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -704,8 +704,5 @@ export function arrayAssignmentNode(strandsContext, bufferNode, indexNode, value // CRITICAL: Record in CFG to preserve sequential ordering CFG.recordInBasicBlock(cfg, cfg.currentBlock, assignmentID); - // Track for global assignments processing - strandsContext.globalAssignments.push(assignmentID); - return { id: assignmentID }; } diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index 29b5575cae..23ac62a4f8 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -212,7 +212,7 @@ const ASTCallbacks = { ]; let isSwizzle = swizzleSets.some(set => [...property].every(char => set.includes(char)) - ) && node.argument.type === 'MemberExpression'; + ) && node.argument.type === 'MemberExpression' && !node.argument.computed; if (isSwizzle) { node.type = 'MemberExpression'; node.object = { @@ -245,6 +245,28 @@ const ASTCallbacks = { node.arguments = []; node.type = 'CallExpression'; }, + MemberExpression(node, _state, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } + // Skip sets -- these will be converted to .set() method + // calls at the AssignmentExpression level + if (ancestors.at(-2)?.type === 'AssignmentExpression') return; + if (node.computed) { + const callee = node.object; + const member = node.property; + node.computed = undefined; + node.object = undefined; + node.callee = { + type: 'MemberExpression', + object: callee, + property: { + type: 'Identifier', + name: 'get', + } + }; + node.arguments = [member]; + node.type = 'CallExpression'; + } + }, VariableDeclarator(node, _state, ancestors) { if (ancestors.some(nodeIsUniform)) { return; } if (nodeIsUniform(node.init)) { From 393baf1c44eceee912f05e7e93e2266249dd3042 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 14 Mar 2026 12:14:15 -0400 Subject: [PATCH 058/250] Add tests for flat arrays --- src/strands/strands_transpiler.js | 7 +- test/unit/visual/cases/webgpu.js | 105 ++++++++++++++++++ .../000.png | Bin 0 -> 305 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 302 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 366 bytes .../metadata.json | 3 + 8 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader reads and transforms float array values/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader reads and transforms float array values/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader writes float values to storage buffer, vertex shader reads them/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader writes float values to storage buffer, vertex shader reads them/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Storage buffer (float array) can be read in a vertex shader for instanced rendering/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Storage buffer (float array) can be read in a vertex shader for instanced rendering/metadata.json diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index 23ac62a4f8..a456b6e9fa 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -249,7 +249,12 @@ const ASTCallbacks = { if (ancestors.some(nodeIsUniform)) { return; } // Skip sets -- these will be converted to .set() method // calls at the AssignmentExpression level - if (ancestors.at(-2)?.type === 'AssignmentExpression') return; + if ( + ancestors.at(-2)?.type === 'AssignmentExpression' && + ancestors.at(-2).left === node + ) { + return; + } if (node.computed) { const callee = node.object; const member = node.property; diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 9dbc344dea..4bcad1bb85 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -996,4 +996,109 @@ visualSuite("WebGPU", function () { await screenshot(); }); }); + + visualSuite('Compute shaders', function() { + visualTest( + 'Storage buffer (float array) can be read in a vertex shader for instanced rendering', + async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + // Positions for 3 spheres: (-15,0), (0,0), (15,0) + const positions = p5.createStorage([-15, 0, 0, 0, 15, 0]); + + const sphereShader = p5.baseMaterialShader().modify(() => { + const posData = p5.uniformStorage(); + p5.getWorldInputs((inputs) => { + const idx = p5.instanceID(); + inputs.position.x += posData[idx * 2]; + inputs.position.y += posData[idx * 2 + 1]; + return inputs; + }); + }, { p5 }); + sphereShader.setUniform('posData', positions); + + const geo = p5.buildGeometry(() => p5.sphere(5)); + p5.background(200); + p5.noStroke(); + p5.fill(255, 0, 0); + p5.shader(sphereShader); + p5.model(geo, 3); + + await screenshot(); + } + ); + + visualTest( + 'Compute shader writes float values to storage buffer, vertex shader reads them', + async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + // Start with zeros; compute shader will write [20, -10] + const offset = p5.createStorage(2); + + const computeShader = p5.buildComputeShader(() => { + const buf = p5.uniformStorage(); + buf[0] = 20; + buf[1] = -10; + }, { p5 }); + computeShader.setUniform('buf', offset); + p5.compute(computeShader, 1); + + const sphereShader = p5.baseMaterialShader().modify(() => { + const buf = p5.uniformStorage(); + p5.getWorldInputs((inputs) => { + inputs.position.x += buf[0]; + inputs.position.y += buf[1]; + return inputs; + }); + }, { p5 }); + sphereShader.setUniform('buf', offset); + + const geo = p5.buildGeometry(() => p5.sphere(5)); + p5.background(200); + p5.noStroke(); + p5.fill(255, 0, 0); + p5.shader(sphereShader); + p5.model(geo, 1); + + await screenshot(); + } + ); + + visualTest( + 'Compute shader reads and transforms float array values', + async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + // Initialize with [10, 0] — compute will double x to get [20, 0] + const buf = p5.createStorage([10, 0]); + + const computeShader = p5.buildComputeShader(() => { + const data = p5.uniformStorage(); + data[0] = data[0] * 2; + }, { p5 }); + computeShader.setUniform('data', buf); + p5.compute(computeShader, 1); + + const sphereShader = p5.baseMaterialShader().modify(() => { + const data = p5.uniformStorage(); + p5.getWorldInputs((inputs) => { + inputs.position.x += data[0]; + inputs.position.y += data[1]; + return inputs; + }); + }, { p5 }); + sphereShader.setUniform('data', buf); + + const geo = p5.buildGeometry(() => p5.sphere(5)); + p5.background(200); + p5.noStroke(); + p5.fill(255, 0, 0); + p5.shader(sphereShader); + p5.model(geo, 1); + + await screenshot(); + } + ); + }); }); diff --git a/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader reads and transforms float array values/000.png b/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader reads and transforms float array values/000.png new file mode 100644 index 0000000000000000000000000000000000000000..cf4799e76be299cdc10fd4ab1af2393152f4d016 GIT binary patch literal 305 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nETXPz#OAr*{oCdG0cG2m$N*5A7K zfAF^uU1r~t%^Dnw+9D_DJUc!!zpBsho$MQs~gjodN>v;Ld_QD~Kj>U0j?>EVD^@_PEh_GJF zUTD60>O{?`s7#kdlWry}&HS?TrVoM{Dm-z^DqE?*k19Dv6Iouo@=#m!$n!>u66b}h s6K|`00($iSe=hTbYCr}X$j8hKHoJ}{FH3NV0E#epy85}Sb4q9e0EonT*8l(j literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader reads and transforms float array values/metadata.json b/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader reads and transforms float array values/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader reads and transforms float array values/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader writes float values to storage buffer, vertex shader reads them/000.png b/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader writes float values to storage buffer, vertex shader reads them/000.png new file mode 100644 index 0000000000000000000000000000000000000000..7e7af5583a7c90b07a864e81cf4f305d829d82c5 GIT binary patch literal 302 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nET$DS^ZAr*{oCI#}f7;w1sPLF#3 z|LD9d^T`p;KNMH6-0I=nctvbRB7YE%&v@+@lDA2w;@_S2J2@vFYH$Pp1WbGBYsz|F0KP{~hRE dHjsCj8LrhIP3}0haXnCk!PC{xWt~$(69CXCgOvaP literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader writes float values to storage buffer, vertex shader reads them/metadata.json b/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader writes float values to storage buffer, vertex shader reads them/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader writes float values to storage buffer, vertex shader reads them/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGPU/Compute shaders/Storage buffer (float array) can be read in a vertex shader for instanced rendering/000.png b/test/unit/visual/screenshots/WebGPU/Compute shaders/Storage buffer (float array) can be read in a vertex shader for instanced rendering/000.png new file mode 100644 index 0000000000000000000000000000000000000000..70a5a6e04fd91d3baa1c115f54abe959cc3f708f GIT binary patch literal 366 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2o;fFe-bxIEGX(z6rX>)odUjT%Yyt zfAIHEX+bN#+%2{oD^5SSX1w>#v?DvGe5!Ohd)e7etZ1vk+zw$u%Ln1!DLGYZg*~Iy zm8=9OZhX1bPbGJ5#|syiW6Q2vP25td^yt6Jr>JR~*BTe@UVXxOzqAE|f&A-)lD&&M zuc*Eb`H@s1@sqP(qDpl-*FpaKD`z=9xHFT(?C^&j{bvpKwBHsH>MVb$`y|WLRJd|_ z(J%X(Jcl1U0j1^gf<093EnV*PfG0~iN^NoyT(;VG7H|80ARu~=U9e&) z!;=Wn_m;~1wfxJ^CD?@>y;-T%BDlUS8k^fo-+p-M(xxjcJg4KvgNI-JQf?R-9_;Mk um5cj7!(|^Zu>SuKyQ6mr$Y28n7c;{uPR1ge(vFKj5e83JKbLh*2~7aal9s#x literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Compute shaders/Storage buffer (float array) can be read in a vertex shader for instanced rendering/metadata.json b/test/unit/visual/screenshots/WebGPU/Compute shaders/Storage buffer (float array) can be read in a vertex shader for instanced rendering/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Compute shaders/Storage buffer (float array) can be read in a vertex shader for instanced rendering/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file From c9b1ffbd09902d18dbbe1d7161c14ef1bfd8220a Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 14 Mar 2026 14:43:37 -0400 Subject: [PATCH 059/250] Initial support for objects in compute shaders --- preview/index.html | 78 +++---- src/strands/ir_builders.js | 76 +++++++ src/strands/strands_api.js | 10 +- src/strands/strands_node.js | 10 +- src/webgpu/p5.RendererWebGPU.js | 175 ++++++++++++---- src/webgpu/strands_wgslBackend.js | 23 +- test/unit/visual/cases/webgpu.js | 198 +++++++++++++++++- .../000.png | Bin 0 -> 304 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 304 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 304 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 304 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 366 bytes .../metadata.json | 3 + 17 files changed, 485 insertions(+), 100 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader reads and updates struct fields (position += velocity)/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader reads and updates struct fields (position += velocity)/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader updates struct fields via intermediate field variable/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader updates struct fields via intermediate field variable/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader updates struct fields via intermediate struct variable/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader updates struct fields via intermediate struct variable/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader writes to struct storage fields, vertex shader reads them/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader writes to struct storage fields, vertex shader reads them/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can be read in a vertex shader for instanced rendering/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can be read in a vertex shader for instanced rendering/metadata.json diff --git a/preview/index.html b/preview/index.html index 61e8fa5899..1408b9798b 100644 --- a/preview/index.html +++ b/preview/index.html @@ -34,11 +34,10 @@ // Compute shader variables let computeShader; - let positionBuffer; - let velocityBuffer; + let particleBuffer; let bouncingCirclesShader; let circleGeometry; - const NUM_CIRCLES = 10_000; + const NUM_CIRCLES = 100; const RADIUS = 2; p.setup = async function () { @@ -93,81 +92,64 @@ }, { p }) // Initialize storage buffers with random positions and velocities - const initialPositions = []; - const initialVelocities = []; - for (let i = 0; i < NUM_CIRCLES * 2; i += 2) { - // Random position (x, y) - store as pairs - initialPositions.push(p.random(-150, 150)); // x - initialPositions.push(p.random(-150, 150)); // y - - // Random velocity (vx, vy) - DVD logo style - initialVelocities.push(0.1 * p.random(1, 3) * (p.random() > 0.5 ? 1 : -1)); // vx - initialVelocities.push(0.1 * p.random(1, 3) * (p.random() > 0.5 ? 1 : -1)); // vy + const initialParticles = []; + for (let i = 0; i < NUM_CIRCLES; i++) { + initialParticles.push({ + position: [p.random(-150, 150), p.random(-150, 150)], + velocity: [ + 0.1 * p.random(1, 3) * (p.random() > 0.5 ? 1 : -1), + 0.1 * p.random(1, 3) * (p.random() > 0.5 ? 1 : -1) + ] + }) } - positionBuffer = p.createStorage(initialPositions); - velocityBuffer = p.createStorage(initialVelocities); + particleBuffer = p.createStorage(initialParticles); // Create compute shader for physics simulation computeShader = p.buildComputeShader(() => { - const positions = p.uniformStorage('positions'); - const velocities = p.uniformStorage('velocities'); + const particles = p.uniformStorage('particles', particleBuffer); const bounds = p.uniformVec2(() => [p.width / 2 - RADIUS, p.height / 2 - RADIUS]); const deltaTime = p.uniformFloat(() => p.deltaTime * 0.1); const idx = iteration.index.x; - // Each circle has 2 floats (x, y) - const posX_idx = idx * 2; - const posY_idx = idx * 2 + 1; - // Read current position and velocity - let posX = positions[posX_idx]; - let posY = positions[posY_idx]; - let velX = velocities[posX_idx]; - let velY = velocities[posY_idx]; + let position = particles[idx].position; + let velocity = particles[idx].velocity; // Update position - posX = posX + velX * deltaTime; - posY = posY + velY * deltaTime; + position += velocity * deltaTime; // Bounce off boundaries - if (posX > bounds.x || posX < -bounds.x) { - velX = -velX; - posX = p.clamp(posX, -bounds.x, bounds.x); + if (position.x > bounds.x || position.x < -bounds.x) { + velocity.x = -velocity.x; + position.x = p.clamp(position.x, -bounds.x, bounds.x); } - if (posY > bounds.y || posY < -bounds.y) { - velY = -velY; - posY = p.clamp(posY, -bounds.y, bounds.y); + if (position.y > bounds.y || position.y < -bounds.y) { + velocity.y = -velocity.y; + position.y = p.clamp(position.y, -bounds.y, bounds.y); } - positions[posX_idx] = posX; - positions[posY_idx] = posY; - velocities[posX_idx] = velX; - velocities[posY_idx] = velY; - }, { p, RADIUS }); + particles[idx].position = position; + particles[idx].velocity = velocity; + }, { p, RADIUS, particleBuffer }); // Shader for rendering bouncing circles from storage buffer bouncingCirclesShader = p.baseMaterialShader().modify(() => { - const positions = p.uniformStorage('positions'); + const particles = p.uniformStorage('particles', particleBuffer); p.getWorldInputs((inputs) => { const instanceIdx = p.instanceID(); - const posX = positions.get(instanceIdx * 2); - const posY = positions.get(instanceIdx * 2 + 1); - - inputs.position.x += posX; - inputs.position.y += posY; + inputs.position.xy += particles[instanceIdx].position; return inputs; }); - }, { p }); + }, { p, particleBuffer }); // Set storage buffers for compute shader - computeShader.setUniform('positions', positionBuffer); - computeShader.setUniform('velocities', velocityBuffer); + computeShader.setUniform('particles', particleBuffer); // Set storage buffer for rendering shader - bouncingCirclesShader.setUniform('positions', positionBuffer); + bouncingCirclesShader.setUniform('particles', particleBuffer); }; p.draw = function () { diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 4e9c7eb700..48898d50c1 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -654,6 +654,82 @@ export function arrayAccessNode(strandsContext, bufferNode, indexNode, accessMod return { id, dimension: 1 }; } +export function createStructArrayElementProxy(strandsContext, bufferNode, indexNode, schema) { + const { dag, cfg } = strandsContext; + + // Ensure index is a StrandsNode + let index; + if (indexNode instanceof StrandsNode) { + index = indexNode; + } else { + const { id, dimension } = primitiveConstructorNode( + strandsContext, + { baseType: BaseType.INT, dimension: 1 }, + indexNode + ); + index = createStrandsNode(id, dimension, strandsContext); + } + + // Create a plain object with getters/setters for each struct field. + // When read, a field creates an ARRAY_ACCESS IR node with the field name encoded + // in the identifier slot. When written, an ASSIGNMENT IR node is recorded in the CFG. + const proxy = {}; + + for (const field of schema.fields) { + Object.defineProperty(proxy, field.name, { + get() { + // Encode field name in identifier so WGSL backend can emit buf[idx].field + const nodeData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Binary.ARRAY_ACCESS, + dependsOn: [bufferNode.id, index.id], + dimension: field.dim, + baseType: BaseType.FLOAT, + identifier: field.name, + }); + const id = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); + return createStrandsNode(id, field.dim, strandsContext); + }, + set(val) { + // Create access node as assignment target (field name in identifier) + const accessData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Binary.ARRAY_ACCESS, + dependsOn: [bufferNode.id, index.id], + dimension: field.dim, + baseType: BaseType.FLOAT, + identifier: field.name, + }); + const accessID = DAG.getOrCreateNode(dag, accessData); + + let valueID; + if (val?.isStrandsNode) { + valueID = val.id; + } else { + const { id } = primitiveConstructorNode( + strandsContext, + { baseType: BaseType.FLOAT, dimension: field.dim }, + val + ); + valueID = id; + } + + const assignData = DAG.createNodeData({ + nodeType: NodeType.ASSIGNMENT, + dependsOn: [accessID, valueID], + phiBlocks: [], + }); + const assignID = DAG.getOrCreateNode(dag, assignData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, assignID); + }, + configurable: true, + }); + } + + return proxy; +} + export function arrayAssignmentNode(strandsContext, bufferNode, indexNode, valueNode) { const { dag, cfg } = strandsContext; diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index f40aad885e..74c2105b43 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -480,7 +480,10 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } // Storage buffer uniform function for compute shaders - fn.uniformStorage = function(name, defaultValue) { + fn.uniformStorage = function(name, bufferOrSchema) { + // Extract schema from a struct storage buffer or explicit schema object + const schema = bufferOrSchema?._schema ?? null; + const { id, dimension } = build.variableNode( strandsContext, { baseType: 'storage', dimension: 1 }, @@ -488,8 +491,8 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { ); strandsContext.uniforms.push({ name, - typeInfo: { baseType: 'storage', dimension: 1 }, - defaultValue, + typeInfo: { baseType: 'storage', dimension: 1, schema }, + defaultValue: bufferOrSchema, }); // Create StrandsNode with _originalIdentifier set (like varying variables) @@ -498,6 +501,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { node._originalIdentifier = name; node._originalBaseType = 'storage'; node._originalDimension = 1; + node._schema = schema; return node; }; } diff --git a/src/strands/strands_node.js b/src/strands/strands_node.js index ae42a413a2..86fcdfc23c 100644 --- a/src/strands/strands_node.js +++ b/src/strands/strands_node.js @@ -1,4 +1,4 @@ -import { swizzleTrap, primitiveConstructorNode, variableNode, arrayAccessNode, arrayAssignmentNode } from './ir_builders'; +import { swizzleTrap, primitiveConstructorNode, variableNode, arrayAccessNode, arrayAssignmentNode, createStructArrayElementProxy } from './ir_builders'; import { BaseType, NodeType, OpCode } from './ir_types'; import { getNodeDataFromID, createNodeData, getOrCreateNode } from './ir_dag'; import { recordInBasicBlock } from './ir_cfg'; @@ -8,6 +8,9 @@ export class StrandsNode { this.strandsContext = strandsContext; this.dimension = dimension; this.structProperties = null; + // Schema for struct storage buffers (set by uniformStorage when buffer has a struct layout). + // When set, buf.get(idx) returns a field proxy instead of a scalar StrandsNode. + this._schema = null; this.isStrandsNode = true; // Store original identifier for varying variables @@ -169,6 +172,11 @@ export class StrandsNode { throw new Error('get() can only be used on storage buffers'); } + // For struct storage, return a proxy with per-field getters/setters + if (this._schema) { + return createStructArrayElementProxy(this.strandsContext, this, index, this._schema); + } + // Create array access node: buffer.get(index) -> buffer[index] const { id, dimension } = arrayAccessNode( this.strandsContext, diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 716ea4c537..87115002ce 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1695,53 +1695,56 @@ function rendererWebGPU(p5, fn) { // SHADER ////////////////////////////////////////////// + // Writes a single field's value into a Float32Array+DataView at (baseOffset + field.offset). + // + // Field interface (shared by uniform fields from _parseStruct and struct storage schema fields): + // baseType: string - 'f32', 'i32', 'u32', etc. + // size: number - byte size of the field + // offset: number - byte offset of the field within its struct + // packInPlace: bool - true for mat3, written with manual column padding + // + // value: number or number[] - the data to write + _packField(field, value, floatView, dataView, baseOffset) { + if (value === undefined) return; + const byteOffset = baseOffset + field.offset; + if (field.baseType === 'u32') { + if (field.size === 4) { + dataView.setUint32(byteOffset, value, true); + } else { + for (let i = 0; i < value.length; i++) { + dataView.setUint32(byteOffset + i * 4, value[i], true); + } + } + } else if (field.baseType === 'i32') { + if (field.size === 4) { + dataView.setInt32(byteOffset, value, true); + } else { + for (let i = 0; i < value.length; i++) { + dataView.setInt32(byteOffset + i * 4, value[i], true); + } + } + } else if (field.packInPlace) { + // In-place packing for mat3: write directly to buffer with padding + const base = byteOffset / 4; + floatView[base + 0] = value[0]; floatView[base + 1] = value[1]; floatView[base + 2] = value[2]; + floatView[base + 4] = value[3]; floatView[base + 5] = value[4]; floatView[base + 6] = value[5]; + floatView[base + 8] = value[6]; floatView[base + 9] = value[7]; floatView[base + 10] = value[8]; + } else if (field.size === 4) { + floatView.set([value], byteOffset / 4); + } else { + floatView.set(value, byteOffset / 4); + } + } + _packUniformGroup(shader, groupUniforms, bufferInfo) { // Pack a single group's uniforms into a buffer const data = bufferInfo.data; const dataView = bufferInfo.dataView; - const offset = bufferInfo.offset || 0; for (const uniform of groupUniforms) { const fullUniform = shader.uniforms[uniform.name]; if (!fullUniform || fullUniform.isSampler) continue; - const uniformData = fullUniform._mappedData; - - if (fullUniform.baseType === 'u32') { - if (fullUniform.size === 4) { - dataView.setUint32(offset + fullUniform.offset, uniformData, true); - } else { - for (let i = 0; i < uniformData.length; i++) { - dataView.setUint32(offset + fullUniform.offset + i * 4, uniformData[i], true); - } - } - } else if (fullUniform.baseType === 'i32') { - if (fullUniform.size === 4) { - dataView.setInt32(offset + fullUniform.offset, uniformData, true); - } else { - for (let i = 0; i < uniformData.length; i++) { - dataView.setInt32(offset + fullUniform.offset + i * 4, uniformData[i], true); - } - } - } else if (fullUniform.packInPlace) { - // In-place packing for mat3: write directly to buffer with padding - const baseOffset = (offset + fullUniform.offset) / 4; - // Column 0 - data[baseOffset + 0] = uniformData[0]; - data[baseOffset + 1] = uniformData[1]; - data[baseOffset + 2] = uniformData[2]; - // Column 1 - data[baseOffset + 4] = uniformData[3]; - data[baseOffset + 5] = uniformData[4]; - data[baseOffset + 6] = uniformData[5]; - // Column 2 - data[baseOffset + 8] = uniformData[6]; - data[baseOffset + 9] = uniformData[7]; - data[baseOffset + 10] = uniformData[8]; - } else if (fullUniform.size === 4) { - data.set([uniformData], (offset + fullUniform.offset) / 4); - } else if (uniformData !== undefined) { - data.set(uniformData, (offset + fullUniform.offset) / 4); - } + this._packField(fullUniform, fullUniform._mappedData, data, dataView, offset); } } @@ -1932,7 +1935,7 @@ function rendererWebGPU(p5, fn) { // Extract storage buffers const storageBuffers = {}; - const storageRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*array/g; + const storageRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*array<\w+>/g; // Track which bindings are taken by the struct properties we've parsed // (the rest should be textures/samplers) @@ -2939,9 +2942,101 @@ ${hookUniformFields}} }; } + // Maps a plain JS value to the WGSL type string that represents it in a struct. + _jsValueToWgslType(value) { + if (typeof value === 'number') return 'f32'; + if (Array.isArray(value)) { + if (value.length === 2) return 'vec2f'; + if (value.length === 3) return 'vec3f'; + if (value.length === 4) return 'vec4f'; + throw new Error(`Unsupported array length ${value.length} for struct storage field`); + } + throw new Error(`Unsupported value type ${typeof value} for struct storage field`); + } + + // Infers a struct schema from the first element of a struct array. + // + // Returns { fields, stride, structBody } where: + // fields: field has the _packField interface (baseType, size, offset, packInPlace) plus: + // name: string - JS property name + // dim: number - float component count, used when creating StrandsNodes + // structBody: everything inside the { ... } of a WGSL struct definition + // stride: how many bytes are reserved for this struct in the buffer + _inferStructSchema(firstElement) { + const entries = Object.entries(firstElement); + + // TODO: if FES is enabled, check if all elements + // share same schema, warn if not. Also check for + // deeply nested objects or other unsupported fields + + const fieldLines = entries.map(([name, value]) => + ` ${name}: ${this._jsValueToWgslType(value)},` + ).join('\n'); + const structBody = `{\n${fieldLines}\n}`; + const elements = this._parseStruct(`struct _Tmp ${structBody}`, '_Tmp'); + + let maxEnd = 0; + let maxAlign = 1; + const fields = entries.map(([name]) => { + const el = elements[name]; + maxEnd = Math.max(maxEnd, el.offsetEnd); + // Alignment for scalars/vectors: <=4 -> 4, <=8 -> 8, else 16 + const align = el.size <= 4 ? 4 : el.size <= 8 ? 8 : 16; + maxAlign = Math.max(maxAlign, align); + return { + name, + baseType: el.baseType, + size: el.size, + offset: el.offset, + packInPlace: el.packInPlace ?? false, + dim: el.size / 4, + }; + }); + + const stride = Math.ceil(maxEnd / maxAlign) * maxAlign; + return { fields, stride, structBody }; + } + + // Packs an array of plain objects into a Float32Array using the given struct schema. + // Reuses _packField so layout rules match uniform packing exactly. + _packStructArray(data, schema) { + const { fields, stride } = schema; + const totalBytes = Math.max(data.length * stride, 16); + const alignedBytes = Math.ceil(totalBytes / 16) * 16; + const buffer = new ArrayBuffer(alignedBytes); + const floatView = new Float32Array(buffer); + const dataView = new DataView(buffer); + for (let i = 0; i < data.length; i++) { + const item = data[i]; + const baseOffset = i * stride; + for (const field of fields) { + this._packField(field, item[field.name], floatView, dataView, baseOffset); + } + } + return floatView; + } + createStorage(dataOrCount) { const device = this.device; + // Struct array: an array of plain objects + if (Array.isArray(dataOrCount) && dataOrCount.length > 0 && + typeof dataOrCount[0] === 'object' && !Array.isArray(dataOrCount[0])) { + const schema = this._inferStructSchema(dataOrCount[0]); + const packed = this._packStructArray(dataOrCount, schema); + const size = packed.byteLength; + const buffer = device.createBuffer({ + size, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, + mappedAtCreation: true, + }); + new Float32Array(buffer.getMappedRange()).set(packed); + buffer.unmap(); + const storageBuffer = { _isStorageBuffer: true, buffer, size, _schema: schema, _renderer: this }; + this._storageBuffers.add(storageBuffer); + return storageBuffer; + } + // Determine buffer size and initial data let size, initialData; if (typeof dataOrCount === 'number') { diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index ce22d1a510..dd8b1b7b56 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -236,13 +236,20 @@ export const wgslBackend = { for (const {name, typeInfo} of strandsContext.uniforms) { if (typeInfo.baseType === 'storage') { + const accessMode = isComputeShader ? 'read_write' : 'read'; + let declaration; + if (typeInfo.schema) { + const structTypeName = `${name}Element`; + declaration = `struct ${structTypeName} ${typeInfo.schema.structBody}\n@group(0) @binding(${bindingIndex}) var ${name}: array<${structTypeName}>;`; + } else { + declaration = `@group(0) @binding(${bindingIndex}) var ${name}: array;`; + } + if (isComputeShader) { - const storageBinding = `@group(0) @binding(${bindingIndex}) var ${name}: array;`; - strandsContext.computeDeclarations.add(storageBinding); + strandsContext.computeDeclarations.add(declaration); } else { - const storageBinding = `@group(0) @binding(${bindingIndex}) var ${name}: array;`; - strandsContext.vertexDeclarations.add(storageBinding); - strandsContext.fragmentDeclarations.add(storageBinding); + strandsContext.vertexDeclarations.add(declaration); + strandsContext.fragmentDeclarations.add(declaration); } bindingIndex += 1; @@ -314,7 +321,8 @@ export const wgslBackend = { const bufferExpr = this.generateExpression(generationContext, dag, bufferID); const indexExpr = this.generateExpression(generationContext, dag, indexID); const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID); - generationContext.write(`${bufferExpr}[i32(${indexExpr})] = ${sourceExpr}${semicolon}`); + const fieldSuffix = targetNode.identifier ? `.${targetNode.identifier}` : ''; + generationContext.write(`${bufferExpr}[i32(${indexExpr})]${fieldSuffix} = ${sourceExpr}${semicolon}`); return; } @@ -478,7 +486,8 @@ export const wgslBackend = { const [bufferID, indexID] = node.dependsOn; const bufferExpr = this.generateExpression(generationContext, dag, bufferID); const indexExpr = this.generateExpression(generationContext, dag, indexID); - return `${bufferExpr}[i32(${indexExpr})]`; + const fieldSuffix = node.identifier ? `.${node.identifier}` : ''; + return `${bufferExpr}[i32(${indexExpr})]${fieldSuffix}`; } if (node.dependsOn.length === 2) { const [lID, rID] = node.dependsOn; diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 4bcad1bb85..f79bd2ccdc 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -1070,7 +1070,7 @@ visualSuite("WebGPU", function () { async function(p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - // Initialize with [10, 0] — compute will double x to get [20, 0] + // Initialize with [10, 0] - compute will double x to get [20, 0] const buf = p5.createStorage([10, 0]); const computeShader = p5.buildComputeShader(() => { @@ -1100,5 +1100,201 @@ visualSuite("WebGPU", function () { await screenshot(); } ); + + + visualTest( + 'Struct storage buffer fields can be read in a vertex shader for instanced rendering', + async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + // Three particles at known positions: left, center, right + const particles = p5.createStorage([ + { position: [-15, 0] }, + { position: [0, 0] }, + { position: [15, 0] }, + ]); + + const sphereShader = p5.baseMaterialShader().modify(() => { + const buf = p5.uniformStorage('buf', particles); + p5.getWorldInputs((inputs) => { + debugger + const p = buf[p5.instanceID()].position; + inputs.position.x += p.x; + inputs.position.y += p.y; + return inputs; + }); + }, { p5, particles }); + sphereShader.setUniform('buf', particles); + + const geo = p5.buildGeometry(() => p5.sphere(5)); + p5.background(200); + p5.noStroke(); + p5.fill(255, 0, 0); + p5.shader(sphereShader); + p5.model(geo, 3); + + await screenshot(); + }, + ); + + visualTest( + 'Compute shader writes to struct storage fields, vertex shader reads them', + async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + const particles = p5.createStorage([ + { position: [0, 0] }, + ]); + + const computeShader = p5.buildComputeShader(() => { + const buf = p5.uniformStorage('buf', particles); + buf[iteration.index.x].position = [15, -10]; + }, { p5, particles }); + computeShader.setUniform('buf', particles); + p5.compute(computeShader, 1); + + const sphereShader = p5.baseMaterialShader().modify(() => { + const buf = p5.uniformStorage('buf', particles); + p5.getWorldInputs((inputs) => { + const pos = buf[0].position; + inputs.position.x += pos.x; + inputs.position.y += pos.y; + return inputs; + }); + }, { p5, particles }); + sphereShader.setUniform('buf', particles); + + const geo = p5.buildGeometry(() => p5.sphere(5)); + p5.background(200); + p5.noStroke(); + p5.fill(255, 0, 0); + p5.shader(sphereShader); + p5.model(geo, 1); + + await screenshot(); + } + ); + + visualTest( + 'Compute shader reads and updates struct fields (position += velocity)', + async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + const particles = p5.createStorage([ + { position: [0, 0], velocity: [15, -10] }, + ]); + + const computeShader = p5.buildComputeShader(() => { + const buf = p5.uniformStorage('buf', particles); + const idx = iteration.index.x; + buf[idx].position = buf[idx].position + buf[idx].velocity; + }, { p5, particles }); + computeShader.setUniform('buf', particles); + p5.compute(computeShader, 1); + + const sphereShader = p5.baseMaterialShader().modify(() => { + const buf = p5.uniformStorage('buf', particles); + p5.getWorldInputs((inputs) => { + const pos = buf[0].position; + inputs.position.x += pos.x; + inputs.position.y += pos.y; + return inputs; + }); + }, { p5, particles }); + sphereShader.setUniform('buf', particles); + + const geo = p5.buildGeometry(() => p5.sphere(5)); + p5.background(200); + p5.noStroke(); + p5.fill(255, 0, 0); + p5.shader(sphereShader); + p5.model(geo, 1); + + await screenshot(); + } + ); + + visualTest( + 'Compute shader updates struct fields via intermediate struct variable', + async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + const particles = p5.createStorage([ + { position: [0, 0], velocity: [15, -10] }, + ]); + + // Store the struct element proxy in a variable and assign through it + const computeShader = p5.buildComputeShader(() => { + const buf = p5.uniformStorage('buf', particles); + const idx = iteration.index.x; + const entry = buf[idx]; + entry.position = entry.position + entry.velocity; + }, { p5, particles }); + computeShader.setUniform('buf', particles); + p5.compute(computeShader, 1); + + const sphereShader = p5.baseMaterialShader().modify(() => { + const buf = p5.uniformStorage('buf', particles); + p5.getWorldInputs((inputs) => { + const pos = buf[0].position; + inputs.position.x += pos.x; + inputs.position.y += pos.y; + return inputs; + }); + }, { p5, particles }); + sphereShader.setUniform('buf', particles); + + const geo = p5.buildGeometry(() => p5.sphere(5)); + p5.background(200); + p5.noStroke(); + p5.fill(255, 0, 0); + p5.shader(sphereShader); + p5.model(geo, 1); + + await screenshot(); + } + ); + + visualTest( + 'Compute shader updates struct fields via intermediate field variable', + async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + const particles = p5.createStorage([ + { position: [0, 0], velocity: [15, -10] }, + ]); + + // Store a field value in an intermediate variable, update it, write it back + const computeShader = p5.buildComputeShader(() => { + const buf = p5.uniformStorage('buf', particles); + const idx = iteration.index.x; + let pos = buf[idx].position; + pos = pos + buf[idx].velocity; + buf[idx].position = pos; + }, { p5, particles }); + computeShader.setUniform('buf', particles); + p5.compute(computeShader, 1); + + const sphereShader = p5.baseMaterialShader().modify(() => { + const buf = p5.uniformStorage('buf', particles); + p5.getWorldInputs((inputs) => { + const pos = buf[0].position; + inputs.position.x += pos.x; + inputs.position.y += pos.y; + return inputs; + }); + }, { p5, particles }); + sphereShader.setUniform('buf', particles); + + const geo = p5.buildGeometry(() => p5.sphere(5)); + p5.background(200); + p5.noStroke(); + p5.fill(255, 0, 0); + p5.shader(sphereShader); + p5.model(geo, 1); + + await screenshot(); + } + ); }); }); diff --git a/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader reads and updates struct fields (position += velocity)/000.png b/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader reads and updates struct fields (position += velocity)/000.png new file mode 100644 index 0000000000000000000000000000000000000000..a561306a22a1275580d6ca7132608d2b8377b7d8 GIT binary patch literal 304 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nETr=Bj3Ar*{oCf(&~b>L`OJl$&V z|EbSca_P57+%Q!RoW_|_Sbt^a`dF89qDl9~erha{_7GB>sKWjuTUGM+E}hOj+ul7q ze7`W~`-gd3m6HxXez3T9_jSJxUa{C{&cN%ojEk+;wp1vkJr^-5UjAr}=*0aUuMfWP z(^g#9b+Wb0=}IB*oXGDc1@}Mcw?>@x50m{R7k6=+)1HGL=Qv%M_-c;JD$ay@AxHOB z#X>998vc5+$gVu+xnvf@H+8|1C06PavzTtocL6HgqdjqhzKGB^ou7=ID$+7~{z{3b zC&^Fn*mUsWr_%yEf&Tpef1V+S3y{GE@-Q=l-kPJyF1K0wfFcZ@u6{1-oD!ML`OJl$&V z|EbSca_P57+%Q!RoW_|_Sbt^a`dF89qDl9~erha{_7GB>sKWjuTUGM+E}hOj+ul7q ze7`W~`-gd3m6HxXez3T9_jSJxUa{C{&cN%ojEk+;wp1vkJr^-5UjAr}=*0aUuMfWP z(^g#9b+Wb0=}IB*oXGDc1@}Mcw?>@x50m{R7k6=+)1HGL=Qv%M_-c;JD$ay@AxHOB z#X>998vc5+$gVu+xnvf@H+8|1C06PavzTtocL6HgqdjqhzKGB^ou7=ID$+7~{z{3b zC&^Fn*mUsWr_%yEf&Tpef1V+S3y{GE@-Q=l-kPJyF1K0wfFcZ@u6{1-oD!ML`OJl$&V z|EbSca_P57+%Q!RoW_|_Sbt^a`dF89qDl9~erha{_7GB>sKWjuTUGM+E}hOj+ul7q ze7`W~`-gd3m6HxXez3T9_jSJxUa{C{&cN%ojEk+;wp1vkJr^-5UjAr}=*0aUuMfWP z(^g#9b+Wb0=}IB*oXGDc1@}Mcw?>@x50m{R7k6=+)1HGL=Qv%M_-c;JD$ay@AxHOB z#X>998vc5+$gVu+xnvf@H+8|1C06PavzTtocL6HgqdjqhzKGB^ou7=ID$+7~{z{3b zC&^Fn*mUsWr_%yEf&Tpef1V+S3y{GE@-Q=l-kPJyF1K0wfFcZ@u6{1-oD!ML`OJl$&V z|EbSca_P57+%Q!RoW_|_Sbt^a`dF89qDl9~erha{_7GB>sKWjuTUGM+E}hOj+ul7q ze7`W~`-gd3m6HxXez3T9_jSJxUa{C{&cN%ojEk+;wp1vkJr^-5UjAr}=*0aUuMfWP z(^g#9b+Wb0=}IB*oXGDc1@}Mcw?>@x50m{R7k6=+)1HGL=Qv%M_-c;JD$ay@AxHOB z#X>998vc5+$gVu+xnvf@H+8|1C06PavzTtocL6HgqdjqhzKGB^ou7=ID$+7~{z{3b zC&^Fn*mUsWr_%yEf&Tpef1V+S3y{GE@-Q=l-kPJyF1K0wfFcZ@u6{1-oD!M)odUjT%Yyt zfAIHEX+bN#+%2{oD^5SSX1w>#v?DvGe5!Ohd)e7etZ1vk+zw$u%Ln1!DLGYZg*~Iy zm8=9OZhX1bPbGJ5#|syiW6Q2vP25td^yt6Jr>JR~*BTe@UVXxOzqAE|f&A-)lD&&M zuc*Eb`H@s1@sqP(qDpl-*FpaKD`z=9xHFT(?C^&j{bvpKwBHsH>MVb$`y|WLRJd|_ z(J%X(Jcl1U0j1^gf<093EnV*PfG0~iN^NoyT(;VG7H|80ARu~=U9e&) z!;=Wn_m;~1wfxJ^CD?@>y;-T%BDlUS8k^fo-+p-M(xxjcJg4KvgNI-JQf?R-9_;Mk um5cj7!(|^Zu>SuKyQ6mr$Y28n7c;{uPR1ge(vFKj5e83JKbLh*2~7aal9s#x literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can be read in a vertex shader for instanced rendering/metadata.json b/test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can be read in a vertex shader for instanced rendering/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can be read in a vertex shader for instanced rendering/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file From d29f025bb3bcc253cbb7e92f5a87f4897b832934 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 14 Mar 2026 14:52:12 -0400 Subject: [PATCH 060/250] Add a way to pass in a schema directly --- src/strands/strands_api.js | 12 +++++- test/unit/visual/cases/webgpu.js | 37 +++++++++++++++++- .../000.png | Bin 0 -> 366 bytes .../metadata.json | 3 ++ 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can be read using an inline schema template/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can be read using an inline schema template/metadata.json diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 74c2105b43..b0e1671fab 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -481,8 +481,16 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { // Storage buffer uniform function for compute shaders fn.uniformStorage = function(name, bufferOrSchema) { - // Extract schema from a struct storage buffer or explicit schema object - const schema = bufferOrSchema?._schema ?? null; + // Extract schema from: + // - a struct storage buffer created by createStorage([{...}, ...]) (_schema is pre-computed) + // - a plain object used as a template, e.g. { position: [0,0], velocity: [0,0] } + // (schema is inferred the same way createStorage does it) + let schema = null; + if (bufferOrSchema?._schema) { + schema = bufferOrSchema._schema; + } else if (bufferOrSchema && !bufferOrSchema._isStorageBuffer && typeof bufferOrSchema === 'object') { + schema = strandsContext.renderer?._inferStructSchema(bufferOrSchema) ?? null; + } const { id, dimension } = build.variableNode( strandsContext, diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index f79bd2ccdc..72db2fdf6e 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -1117,7 +1117,6 @@ visualSuite("WebGPU", function () { const sphereShader = p5.baseMaterialShader().modify(() => { const buf = p5.uniformStorage('buf', particles); p5.getWorldInputs((inputs) => { - debugger const p = buf[p5.instanceID()].position; inputs.position.x += p.x; inputs.position.y += p.y; @@ -1134,7 +1133,41 @@ visualSuite("WebGPU", function () { p5.model(geo, 3); await screenshot(); - }, + } + ); + + visualTest( + 'Struct storage buffer fields can be read using an inline schema template', + async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + // Same layout as above but schema is declared inline rather than via the buffer + const particles = p5.createStorage([ + { position: [-15, 0] }, + { position: [0, 0] }, + { position: [15, 0] }, + ]); + + const sphereShader = p5.baseMaterialShader().modify(() => { + const buf = p5.uniformStorage('buf', { position: [0, 0] }); + p5.getWorldInputs((inputs) => { + const p = buf[p5.instanceID()].position; + inputs.position.x += p.x; + inputs.position.y += p.y; + return inputs; + }); + }, { p5 }); + sphereShader.setUniform('buf', particles); + + const geo = p5.buildGeometry(() => p5.sphere(5)); + p5.background(200); + p5.noStroke(); + p5.fill(255, 0, 0); + p5.shader(sphereShader); + p5.model(geo, 3); + + await screenshot(); + } ); visualTest( diff --git a/test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can be read using an inline schema template/000.png b/test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can be read using an inline schema template/000.png new file mode 100644 index 0000000000000000000000000000000000000000..70a5a6e04fd91d3baa1c115f54abe959cc3f708f GIT binary patch literal 366 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2o;fFe-bxIEGX(z6rX>)odUjT%Yyt zfAIHEX+bN#+%2{oD^5SSX1w>#v?DvGe5!Ohd)e7etZ1vk+zw$u%Ln1!DLGYZg*~Iy zm8=9OZhX1bPbGJ5#|syiW6Q2vP25td^yt6Jr>JR~*BTe@UVXxOzqAE|f&A-)lD&&M zuc*Eb`H@s1@sqP(qDpl-*FpaKD`z=9xHFT(?C^&j{bvpKwBHsH>MVb$`y|WLRJd|_ z(J%X(Jcl1U0j1^gf<093EnV*PfG0~iN^NoyT(;VG7H|80ARu~=U9e&) z!;=Wn_m;~1wfxJ^CD?@>y;-T%BDlUS8k^fo-+p-M(xxjcJg4KvgNI-JQf?R-9_;Mk um5cj7!(|^Zu>SuKyQ6mr$Y28n7c;{uPR1ge(vFKj5e83JKbLh*2~7aal9s#x literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can be read using an inline schema template/metadata.json b/test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can be read using an inline schema template/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can be read using an inline schema template/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file From f20d5ac996d7eaae4be1b0c8a6ae0611cc18dd82 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 14 Mar 2026 15:50:17 -0400 Subject: [PATCH 061/250] Allow passing in default values --- preview/index.html | 6 ------ src/strands/strands_api.js | 29 ++++++++++++++++++++--------- src/strands/strands_codegen.js | 13 ++++++++++--- src/webgl/p5.Shader.js | 12 ++++++++++++ test/unit/visual/cases/webgpu.js | 9 --------- 5 files changed, 42 insertions(+), 27 deletions(-) diff --git a/preview/index.html b/preview/index.html index 1408b9798b..5afac77ed2 100644 --- a/preview/index.html +++ b/preview/index.html @@ -144,12 +144,6 @@ return inputs; }); }, { p, particleBuffer }); - - // Set storage buffers for compute shader - computeShader.setUniform('particles', particleBuffer); - - // Set storage buffer for rendering shader - bouncingCirclesShader.setUniform('particles', particleBuffer); }; p.draw = function () { diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index b0e1671fab..41fe82d243 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -481,15 +481,26 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { // Storage buffer uniform function for compute shaders fn.uniformStorage = function(name, bufferOrSchema) { - // Extract schema from: - // - a struct storage buffer created by createStorage([{...}, ...]) (_schema is pre-computed) - // - a plain object used as a template, e.g. { position: [0,0], velocity: [0,0] } - // (schema is inferred the same way createStorage does it) let schema = null; - if (bufferOrSchema?._schema) { - schema = bufferOrSchema._schema; - } else if (bufferOrSchema && !bufferOrSchema._isStorageBuffer && typeof bufferOrSchema === 'object') { - schema = strandsContext.renderer?._inferStructSchema(bufferOrSchema) ?? null; + let defaultValue = null; + + // If it's a function, evaluate it immediately to infer schema, + // then store the function so it gets called each frame. + let value = bufferOrSchema; + if (typeof bufferOrSchema === 'function') { + value = bufferOrSchema(); + if (value?._schema) { + defaultValue = bufferOrSchema; + } + } + + if (value?._schema) { + // Struct storage buffer with pre-computed schema + schema = value._schema; + if (defaultValue === null) defaultValue = value; + } else if (value && typeof value === 'object' && !value._isStorageBuffer) { + // Plain object schema template -- only used to infer struct layout, not as a default value + schema = strandsContext.renderer?._inferStructSchema(value) ?? null; } const { id, dimension } = build.variableNode( @@ -500,7 +511,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { strandsContext.uniforms.push({ name, typeInfo: { baseType: 'storage', dimension: 1, schema }, - defaultValue: bufferOrSchema, + defaultValue, }); // Create StrandsNode with _originalIdentifier set (like varying variables) diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 8d93877e94..d09a00e812 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -13,13 +13,20 @@ export function generateShaderCode(strandsContext) { const hooksObj = { uniforms: {}, + storageUniforms: {}, varyingVariables: [], }; for (const {name, typeInfo, defaultValue} of strandsContext.uniforms) { - const key = backend.generateHookUniformKey(name, typeInfo); - if (key !== null) { - hooksObj.uniforms[key] = defaultValue; + if (typeInfo.baseType === 'storage') { + if (defaultValue !== null && defaultValue !== undefined) { + hooksObj.storageUniforms[name] = defaultValue; + } + } else { + const key = backend.generateHookUniformKey(name, typeInfo); + if (key !== null) { + hooksObj.uniforms[key] = defaultValue; + } } } diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 9659d0a6cd..7c9089fb9f 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -49,6 +49,9 @@ class Shader { // Stores uniforms + default values. uniforms: options.uniforms || {}, + // Compute shader storage uniforms + default values + storageUniforms: options.storageUniforms || {}, + // Stores custom uniform + helper declarations as a string. declarations: options.declarations, @@ -409,6 +412,7 @@ class Shader { for (const key in hooks) { if (key === 'declarations') continue; if (key === 'uniforms') continue; + if (key === 'storageUniforms') continue; if (key === 'varyingVariables') continue; if (key === 'vertexDeclarations') { newHooks.vertex.declarations = @@ -455,6 +459,7 @@ class Shader { declarations: (this.hooks.declarations || '') + '\n' + (hooks.declarations || ''), uniforms: Object.assign({}, this.hooks.uniforms, hooks.uniforms || {}), + storageUniforms: Object.assign({}, this.hooks.storageUniforms, hooks.storageUniforms || {}), varyingVariables: (hooks.varyingVariables || []).concat(this.hooks.varyingVariables || []), fragment: Object.assign({}, this.hooks.fragment, newHooks.fragment || {}), vertex: Object.assign({}, this.hooks.vertex, newHooks.vertex || {}), @@ -520,6 +525,13 @@ class Shader { this.setUniform(name, value); } } + for (const name in this.hooks.storageUniforms) { + const initializer = this.hooks.storageUniforms[name]; + const value = initializer instanceof Function ? initializer() : initializer; + if (value !== undefined && value !== null) { + this.setUniform(name, value); + } + } } /** diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 72db2fdf6e..0450ff85cc 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -1123,7 +1123,6 @@ visualSuite("WebGPU", function () { return inputs; }); }, { p5, particles }); - sphereShader.setUniform('buf', particles); const geo = p5.buildGeometry(() => p5.sphere(5)); p5.background(200); @@ -1183,7 +1182,6 @@ visualSuite("WebGPU", function () { const buf = p5.uniformStorage('buf', particles); buf[iteration.index.x].position = [15, -10]; }, { p5, particles }); - computeShader.setUniform('buf', particles); p5.compute(computeShader, 1); const sphereShader = p5.baseMaterialShader().modify(() => { @@ -1195,7 +1193,6 @@ visualSuite("WebGPU", function () { return inputs; }); }, { p5, particles }); - sphereShader.setUniform('buf', particles); const geo = p5.buildGeometry(() => p5.sphere(5)); p5.background(200); @@ -1222,7 +1219,6 @@ visualSuite("WebGPU", function () { const idx = iteration.index.x; buf[idx].position = buf[idx].position + buf[idx].velocity; }, { p5, particles }); - computeShader.setUniform('buf', particles); p5.compute(computeShader, 1); const sphereShader = p5.baseMaterialShader().modify(() => { @@ -1234,7 +1230,6 @@ visualSuite("WebGPU", function () { return inputs; }); }, { p5, particles }); - sphereShader.setUniform('buf', particles); const geo = p5.buildGeometry(() => p5.sphere(5)); p5.background(200); @@ -1263,7 +1258,6 @@ visualSuite("WebGPU", function () { const entry = buf[idx]; entry.position = entry.position + entry.velocity; }, { p5, particles }); - computeShader.setUniform('buf', particles); p5.compute(computeShader, 1); const sphereShader = p5.baseMaterialShader().modify(() => { @@ -1275,7 +1269,6 @@ visualSuite("WebGPU", function () { return inputs; }); }, { p5, particles }); - sphereShader.setUniform('buf', particles); const geo = p5.buildGeometry(() => p5.sphere(5)); p5.background(200); @@ -1305,7 +1298,6 @@ visualSuite("WebGPU", function () { pos = pos + buf[idx].velocity; buf[idx].position = pos; }, { p5, particles }); - computeShader.setUniform('buf', particles); p5.compute(computeShader, 1); const sphereShader = p5.baseMaterialShader().modify(() => { @@ -1317,7 +1309,6 @@ visualSuite("WebGPU", function () { return inputs; }); }, { p5, particles }); - sphereShader.setUniform('buf', particles); const geo = p5.buildGeometry(() => p5.sphere(5)); p5.background(200); From 8c2970f168f038e22694cc13b77c84840e5f7cc2 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 14 Mar 2026 16:04:22 -0400 Subject: [PATCH 062/250] Support vectors as inputs --- src/math/p5.Vector.js | 1 + src/webgpu/p5.RendererWebGPU.js | 9 +++++ test/unit/visual/cases/webgpu.js | 33 ++++++++++++++++++ .../000.png | Bin 0 -> 366 bytes .../metadata.json | 3 ++ 5 files changed, 46 insertions(+) create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can use p5.Vector values/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can use p5.Vector values/metadata.json diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 1f3df37f8a..8cd8334072 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -48,6 +48,7 @@ class Vector { this.dimensions = dimensions; this._values = values; } + this.isVector = true; } /** diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 87115002ce..d56203fea9 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1706,6 +1706,9 @@ function rendererWebGPU(p5, fn) { // value: number or number[] - the data to write _packField(field, value, floatView, dataView, baseOffset) { if (value === undefined) return; + if (value?.isVector) { + value = value.values.length !== value.dimensions ? value.values.slice(0, value.dimensions) : value.values; + } const byteOffset = baseOffset + field.offset; if (field.baseType === 'u32') { if (field.size === 4) { @@ -2945,6 +2948,12 @@ ${hookUniformFields}} // Maps a plain JS value to the WGSL type string that represents it in a struct. _jsValueToWgslType(value) { if (typeof value === 'number') return 'f32'; + if (value?.isVector) { + if (value.dimensions === 2) return 'vec2f'; + if (value.dimensions === 3) return 'vec3f'; + if (value.dimensions === 4) return 'vec4f'; + throw new Error(`Unsupported vector dimension ${value.dimensions} for struct storage field`); + } if (Array.isArray(value)) { if (value.length === 2) return 'vec2f'; if (value.length === 3) return 'vec3f'; diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 0450ff85cc..6f4deb5677 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -1135,6 +1135,39 @@ visualSuite("WebGPU", function () { } ); + visualTest( + 'Struct storage buffer fields can use p5.Vector values', + async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + // Three particles at known positions: left, center, right + const particles = p5.createStorage([ + { position: p5.createVector(-15, 0) }, + { position: p5.createVector(0, 0) }, + { position: p5.createVector(15, 0) }, + ]); + + const sphereShader = p5.baseMaterialShader().modify(() => { + const buf = p5.uniformStorage('buf', particles); + p5.getWorldInputs((inputs) => { + const p = buf[p5.instanceID()].position; + inputs.position.x += p.x; + inputs.position.y += p.y; + return inputs; + }); + }, { p5, particles }); + + const geo = p5.buildGeometry(() => p5.sphere(5)); + p5.background(200); + p5.noStroke(); + p5.fill(255, 0, 0); + p5.shader(sphereShader); + p5.model(geo, 3); + + await screenshot(); + } + ); + visualTest( 'Struct storage buffer fields can be read using an inline schema template', async function(p5, screenshot) { diff --git a/test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can use p5.Vector values/000.png b/test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can use p5.Vector values/000.png new file mode 100644 index 0000000000000000000000000000000000000000..70a5a6e04fd91d3baa1c115f54abe959cc3f708f GIT binary patch literal 366 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2o;fFe-bxIEGX(z6rX>)odUjT%Yyt zfAIHEX+bN#+%2{oD^5SSX1w>#v?DvGe5!Ohd)e7etZ1vk+zw$u%Ln1!DLGYZg*~Iy zm8=9OZhX1bPbGJ5#|syiW6Q2vP25td^yt6Jr>JR~*BTe@UVXxOzqAE|f&A-)lD&&M zuc*Eb`H@s1@sqP(qDpl-*FpaKD`z=9xHFT(?C^&j{bvpKwBHsH>MVb$`y|WLRJd|_ z(J%X(Jcl1U0j1^gf<093EnV*PfG0~iN^NoyT(;VG7H|80ARu~=U9e&) z!;=Wn_m;~1wfxJ^CD?@>y;-T%BDlUS8k^fo-+p-M(xxjcJg4KvgNI-JQf?R-9_;Mk um5cj7!(|^Zu>SuKyQ6mr$Y28n7c;{uPR1ge(vFKj5e83JKbLh*2~7aal9s#x literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can use p5.Vector values/metadata.json b/test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can use p5.Vector values/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Compute shaders/Struct storage buffer fields can use p5.Vector values/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file From 0160840d9ba4598b5aca5570ce4b1185f2c6a7ab Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 14 Mar 2026 16:10:27 -0400 Subject: [PATCH 063/250] Support full-object assignment to array indices --- src/strands/strands_node.js | 11 +++++ test/unit/visual/cases/webgpu.js | 40 ++++++++++++++++++ .../000.png | Bin 0 -> 304 bytes .../metadata.json | 3 ++ 4 files changed, 54 insertions(+) create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader writes a whole struct element as an object literal/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader writes a whole struct element as an object literal/metadata.json diff --git a/src/strands/strands_node.js b/src/strands/strands_node.js index 86fcdfc23c..1528a0fc3c 100644 --- a/src/strands/strands_node.js +++ b/src/strands/strands_node.js @@ -197,6 +197,17 @@ export class StrandsNode { throw new Error('set() can only be used on storage buffers with an identifier'); } + // If value is a plain object (struct literal), expand to per-field assignments + // e.g. buf[idx] = { position: pos, velocity: vel } + // becomes buf[idx].position = pos; buf[idx].velocity = vel; + if (value !== null && typeof value === 'object' && !value.isStrandsNode && this._schema) { + const proxy = createStructArrayElementProxy(this.strandsContext, this, index, this._schema); + for (const [fieldName, fieldValue] of Object.entries(value)) { + proxy[fieldName] = fieldValue; + } + return this; + } + // Create array assignment node: buffer.set(index, value) -> buffer[index] = value // This creates an ASSIGNMENT node and records it in the CFG basic block // CFG preserves sequential order, preventing reordering of assignments diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 6f4deb5677..bd79d18e62 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -1353,5 +1353,45 @@ visualSuite("WebGPU", function () { await screenshot(); } ); + + visualTest( + 'Compute shader writes a whole struct element as an object literal', + async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + const particles = p5.createStorage([ + { position: [0, 0], velocity: [15, -10] }, + ]); + + const computeShader = p5.buildComputeShader(() => { + const buf = p5.uniformStorage('buf', particles); + const idx = iteration.index.x; + let pos = buf[idx].position; + let vel = buf[idx].velocity; + pos = pos + vel; + buf[idx] = { position: pos, velocity: vel }; + }, { p5, particles }); + p5.compute(computeShader, 1); + + const sphereShader = p5.baseMaterialShader().modify(() => { + const buf = p5.uniformStorage('buf', particles); + p5.getWorldInputs((inputs) => { + const pos = buf[0].position; + inputs.position.x += pos.x; + inputs.position.y += pos.y; + return inputs; + }); + }, { p5, particles }); + + const geo = p5.buildGeometry(() => p5.sphere(5)); + p5.background(200); + p5.noStroke(); + p5.fill(255, 0, 0); + p5.shader(sphereShader); + p5.model(geo, 1); + + await screenshot(); + } + ); }); }); diff --git a/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader writes a whole struct element as an object literal/000.png b/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader writes a whole struct element as an object literal/000.png new file mode 100644 index 0000000000000000000000000000000000000000..a561306a22a1275580d6ca7132608d2b8377b7d8 GIT binary patch literal 304 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nETr=Bj3Ar*{oCf(&~b>L`OJl$&V z|EbSca_P57+%Q!RoW_|_Sbt^a`dF89qDl9~erha{_7GB>sKWjuTUGM+E}hOj+ul7q ze7`W~`-gd3m6HxXez3T9_jSJxUa{C{&cN%ojEk+;wp1vkJr^-5UjAr}=*0aUuMfWP z(^g#9b+Wb0=}IB*oXGDc1@}Mcw?>@x50m{R7k6=+)1HGL=Qv%M_-c;JD$ay@AxHOB z#X>998vc5+$gVu+xnvf@H+8|1C06PavzTtocL6HgqdjqhzKGB^ou7=ID$+7~{z{3b zC&^Fn*mUsWr_%yEf&Tpef1V+S3y{GE@-Q=l-kPJyF1K0wfFcZ@u6{1-oD!M Date: Sat, 14 Mar 2026 16:22:58 -0400 Subject: [PATCH 064/250] Add FES checks --- src/webgpu/p5.RendererWebGPU.js | 51 +++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index d56203fea9..59c863373e 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -2974,9 +2974,22 @@ ${hookUniformFields}} _inferStructSchema(firstElement) { const entries = Object.entries(firstElement); - // TODO: if FES is enabled, check if all elements - // share same schema, warn if not. Also check for - // deeply nested objects or other unsupported fields + if (!p5.disableFriendlyErrors) { + for (const [name, value] of entries) { + if ( + value !== null && + typeof value === 'object' && + !Array.isArray(value) && + !value?.isVector + ) { + p5._friendlyError( + `The "${name}" property in your storage data contains a nested object. ` + + `Make sure you only use properties with numbers, arrays of numbers, or p5.Vector.`, + 'createStorage' + ); + } + } + } const fieldLines = entries.map(([name, value]) => ` ${name}: ${this._jsValueToWgslType(value)},` @@ -3031,6 +3044,38 @@ ${hookUniformFields}} // Struct array: an array of plain objects if (Array.isArray(dataOrCount) && dataOrCount.length > 0 && typeof dataOrCount[0] === 'object' && !Array.isArray(dataOrCount[0])) { + if (!p5.disableFriendlyErrors && dataOrCount.length > 1) { + const firstKeys = Object.keys(dataOrCount[0]); + let warned = false; + for (let i = 1; i < dataOrCount.length; i++) { + const el = dataOrCount[i]; + const elKeys = Object.keys(el); + const sameKeys = firstKeys.length === elKeys.length && + firstKeys.every((k, j) => k === elKeys[j]); + if (!sameKeys) { + p5._friendlyError( + `Element ${i} has different fields than element 0. ` + + `All elements should have the same properties.`, + 'createStorage' + ); + break; + } + for (const key of firstKeys) { + const firstType = this._jsValueToWgslType(dataOrCount[0][key]); + const elType = this._jsValueToWgslType(el[key]); + if (firstType !== elType) { + p5._friendlyError( + `The "${key}" property of element ${i} has type ${elType} ` + + `but element 0 has type ${firstType}. Proporties should have the same type across all elements.`, + 'createStorage' + ); + warned = true; + break; + } + } + if (warned) break; + } + } const schema = this._inferStructSchema(dataOrCount[0]); const packed = this._packStructArray(dataOrCount, schema); const size = packed.byteLength; From a5f85a7bbb4bb8e933f26244c81b6567110f9873 Mon Sep 17 00:00:00 2001 From: Kathrina-dev Date: Sun, 15 Mar 2026 06:21:47 +0000 Subject: [PATCH 065/250] Fix TypeScript typing for filterColor shader hook --- utils/patch.mjs | 51 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/utils/patch.mjs b/utils/patch.mjs index 446a6ef755..441b99d2a5 100644 --- a/utils/patch.mjs +++ b/utils/patch.mjs @@ -3,18 +3,18 @@ import fs from 'fs'; export function applyPatches() { const cache = {}; const patched = {}; - + const replace = (path, src, dest) => { if (Array.isArray(path)) { path.forEach(path => replace(path, src, dest)); return; } try { - if (!path.startsWith("types/")) - path = "types/" + path; + if (!path.startsWith('types/')) + path = 'types/' + path; const before = patched[path] ?? - (cache[path] ??= fs.readFileSync("./" + path, { encoding: 'utf-8' })); + (cache[path] ??= fs.readFileSync('./' + path, { encoding: 'utf-8' })); const after = before.replaceAll(src, dest); if (after !== before) @@ -28,8 +28,8 @@ export function applyPatches() { // TODO: Handle this better in the docs instead of patching replace( - "p5.d.ts", - "constructor(detailX?: number, detailY?: number, callback?: Function);", + 'p5.d.ts', + 'constructor(detailX?: number, detailY?: number, callback?: Function);', `constructor( detailX?: number, detailY?: number, @@ -39,9 +39,9 @@ export function applyPatches() { // https://github.com/p5-types/p5.ts/issues/31 // #todo: add readonly to appropriate array params, either here or in doc comments replace( - ["p5.d.ts", "global.d.ts"], - "random(choices: any[]): any;", - "random(choices: readonly T[]): T;" + ['p5.d.ts', 'global.d.ts'], + 'random(choices: any[]): any;', + 'random(choices: readonly T[]): T;' ); replace( @@ -53,7 +53,7 @@ export function applyPatches() { replace( 'p5.d.ts', 'textToContours(str: string, x: number, y: number, options?: { sampleFactor?: number; simplifyThreshold?: number }): object[][];', - 'textToContours(str: string, x: number, y: number, options?: { sampleFactor?: number; simplifyThreshold?: number }): { x: number; y: number; alpha: number }[][];', + 'textToContours(str: string, x: number, y: number, options?: { sampleFactor?: number; simplifyThreshold?: number }): { x: number; y: number; alpha: number }[][];' ); replace( @@ -77,7 +77,7 @@ export function applyPatches() { 'class __Graphics extends p5.Element {', `class __Graphics extends p5.Element { elt: HTMLCanvasElement; - `, + ` ); // Type .elt more specifically for audio and video elements @@ -86,24 +86,24 @@ export function applyPatches() { `class MediaElement extends Element { elt: HTMLAudioElement | HTMLVideoElement;`, `class MediaElement extends Element { - elt: T;`, + elt: T;` ); replace( ['p5.d.ts', 'global.d.ts'], /createAudio\(src\?: string \| string\[\], callback\?: Function\): ([pP]5)\.MediaElement;/g, - 'createAudio(src?: string | string[], callback?: (video: $1.MediaElement) => any): $1.MediaElement;', + 'createAudio(src?: string | string[], callback?: (video: $1.MediaElement) => any): $1.MediaElement;' ); replace( ['p5.d.ts', 'global.d.ts'], /createVideo\(src\?: string \| string\[\], callback\?: Function\): ([pP]5)\.MediaElement;/g, - 'createVideo(src?: string | string[], callback?: (video: $1.MediaElement) => any): $1.MediaElement;', + 'createVideo(src?: string | string[], callback?: (video: $1.MediaElement) => any): $1.MediaElement;' ); // More callback types replace( ['p5.d.ts', 'global.d.ts'], /createFileInput\(callback: Function, multiple\?: boolean\): ([pP]5)\.Element;/g, - 'createFileInput(callback: (input: $1.File) => any, multiple?: boolean): $1.Element;', + 'createFileInput(callback: (input: $1.File) => any, multiple?: boolean): $1.Element;' ); replace( ['p5.d.ts', 'global.d.ts'], @@ -120,7 +120,7 @@ export function applyPatches() { replace( 'p5.d.ts', 'fontBounds(str: string, x: number, y: number, width?: number, height?: number): object;', - 'fontBounds(str: string, x: number, y: number, width?: number, height?: number): { x: number; y: number; w: number; h: number };', + 'fontBounds(str: string, x: number, y: number, width?: number, height?: number): { x: number; y: number; w: number; h: number };' ); replace( 'p5.d.ts', @@ -130,7 +130,7 @@ export function applyPatches() { replace( 'p5.d.ts', 'textBounds(str: string, x: number, y: number, width?: number, height?: number): object;', - 'textBounds(str: string, x: number, y: number, width?: number, height?: number): { x: number; y: number; w: number; h: number };', + 'textBounds(str: string, x: number, y: number, width?: number, height?: number): { x: number; y: number; w: number; h: number };' ); // Document Typr @@ -166,10 +166,25 @@ export function applyPatches() { ` ); + // Fix filterColor hook typing + replace( + ['p5.d.ts'], + 'declare const filterColor: object;', + `declare const filterColor: { + texCoord: any; + canvasSize: any; + texelSize: any; + canvasContent: any; + begin(): void; + end(): void; + set(color: any): void; + };` + ); + for (const [path, data] of Object.entries(patched)) { try { console.log(`Patched ${path}`); - fs.writeFileSync("./" + path, data); + fs.writeFileSync('./' + path, data); } catch (err) { console.error(err); } From 6525f746506799c9ca5fff711a2bde969a2dc470 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 15 Mar 2026 15:02:06 -0400 Subject: [PATCH 066/250] Start adding WebGPU to docs --- docs/parameterData.json | 440 +++++++++++++++++--------------- src/strands/p5.strands.js | 33 +++ src/webgl/p5.Shader.js | 2 +- src/webgpu/p5.RendererWebGPU.js | 158 +++++++++++- utils/data-processor.mjs | 6 +- 5 files changed, 425 insertions(+), 214 deletions(-) diff --git a/docs/parameterData.json b/docs/parameterData.json index 6cf76f3c4a..14ccd90ad1 100644 --- a/docs/parameterData.json +++ b/docs/parameterData.json @@ -486,6 +486,37 @@ ] ] }, + "ambientLight": { + "overloads": [ + [ + "Number", + "Number", + "Number", + "Number?" + ], + [ + "Number", + "Number?" + ], + [ + "String" + ], + [ + "Number[]" + ], + [ + "p5.Color" + ] + ] + }, + "selectAll": { + "overloads": [ + [ + "String", + "String|p5.Element|HTMLElement?" + ] + ] + }, "smoothstep": { "overloads": [ [ @@ -495,6 +526,17 @@ ] ] }, + "uniformStorage": { + "overloads": [ + [ + "String", + "p5.StorageBuffer|Function|Object?" + ], + [ + "p5.StorageBuffer|Function|Object?" + ] + ] + }, "getTexture": { "overloads": [ [ @@ -545,37 +587,6 @@ ] ] }, - "ambientLight": { - "overloads": [ - [ - "Number", - "Number", - "Number", - "Number?" - ], - [ - "Number", - "Number?" - ], - [ - "String" - ], - [ - "Number[]" - ], - [ - "p5.Color" - ] - ] - }, - "selectAll": { - "overloads": [ - [ - "String", - "String|p5.Element|HTMLElement?" - ] - ] - }, "bezier": { "overloads": [ [ @@ -1408,23 +1419,6 @@ ] ] }, - "setup": { - "overloads": [ - [] - ] - }, - "draw": { - "overloads": [ - [] - ] - }, - "registerAddon": { - "overloads": [ - [ - "Function" - ] - ] - }, "map": { "overloads": [ [ @@ -1624,6 +1618,23 @@ ] ] }, + "setup": { + "overloads": [ + [] + ] + }, + "draw": { + "overloads": [ + [] + ] + }, + "registerAddon": { + "overloads": [ + [ + "Function" + ] + ] + }, "clear": { "overloads": [ [ @@ -1782,15 +1793,6 @@ ] ] }, - "loadBytes": { - "overloads": [ - [ - "String|Request", - "Function?", - "Function?" - ] - ] - }, "noDebugMode": { "overloads": [ [] @@ -1826,6 +1828,15 @@ [] ] }, + "loadBytes": { + "overloads": [ + [ + "String|Request", + "Function?", + "Function?" + ] + ] + }, "buildFilterShader": { "overloads": [ [ @@ -1880,15 +1891,6 @@ ] ] }, - "loadBlob": { - "overloads": [ - [ - "String|Request", - "Function?", - "Function?" - ] - ] - }, "pow": { "overloads": [ [ @@ -1904,6 +1906,15 @@ ] ] }, + "loadBlob": { + "overloads": [ + [ + "String|Request", + "Function?", + "Function?" + ] + ] + }, "pointLight": { "overloads": [ [ @@ -2014,18 +2025,10 @@ ] ] }, - "httpGet": { + "imageLight": { "overloads": [ [ - "String|Request", - "String?", - "Function?", - "Function?" - ], - [ - "String|Request", - "Function", - "Function?" + "p5.Image" ] ] }, @@ -2037,10 +2040,18 @@ [] ] }, - "imageLight": { + "httpGet": { "overloads": [ [ - "p5.Image" + "String|Request", + "String?", + "Function?", + "Function?" + ], + [ + "String|Request", + "Function", + "Function?" ] ] }, @@ -2108,28 +2119,6 @@ ] ] }, - "httpPost": { - "overloads": [ - [ - "String|Request", - "Object|Boolean?", - "String?", - "Function?", - "Function?" - ], - [ - "String|Request", - "Object|Boolean", - "Function?", - "Function?" - ], - [ - "String|Request", - "Function?", - "Function?" - ] - ] - }, "unhex": { "overloads": [ [ @@ -2179,6 +2168,28 @@ ] ] }, + "httpPost": { + "overloads": [ + [ + "String|Request", + "Object|Boolean?", + "String?", + "Function?", + "Function?" + ], + [ + "String|Request", + "Object|Boolean", + "Function?", + "Function?" + ], + [ + "String|Request", + "Function?", + "Function?" + ] + ] + }, "cylinder": { "overloads": [ [ @@ -2261,11 +2272,6 @@ ] ] }, - "getURLParams": { - "overloads": [ - [] - ] - }, "colorMode": { "overloads": [ [ @@ -2282,6 +2288,11 @@ [] ] }, + "getURLParams": { + "overloads": [ + [] + ] + }, "shearX": { "overloads": [ [ @@ -2326,14 +2337,6 @@ ] ] }, - "model": { - "overloads": [ - [ - "p5.Geometry", - "Number?" - ] - ] - }, "setContent": { "overloads": [ [ @@ -2341,20 +2344,11 @@ ] ] }, - "httpDo": { + "model": { "overloads": [ [ - "String|Request", - "String?", - "String?", - "Object?", - "Function?", - "Function?" - ], - [ - "String|Request", - "Function?", - "Function?" + "p5.Geometry", + "Number?" ] ] }, @@ -2380,6 +2374,23 @@ ] ] }, + "httpDo": { + "overloads": [ + [ + "String|Request", + "String?", + "String?", + "Object?", + "Function?", + "Function?" + ], + [ + "String|Request", + "Function?", + "Function?" + ] + ] + }, "shearY": { "overloads": [ [ @@ -2490,6 +2501,13 @@ ] ] }, + "brightness": { + "overloads": [ + [ + "p5.Color|Number[]|String" + ] + ] + }, "tint": { "overloads": [ [ @@ -2513,21 +2531,6 @@ ] ] }, - "brightness": { - "overloads": [ - [ - "p5.Color|Number[]|String" - ] - ] - }, - "createWriter": { - "overloads": [ - [ - "String", - "String?" - ] - ] - }, "spotLight": { "overloads": [ [ @@ -2612,6 +2615,14 @@ ] ] }, + "createWriter": { + "overloads": [ + [ + "String", + "String?" + ] + ] + }, "translate": { "overloads": [ [ @@ -2838,24 +2849,24 @@ [] ] }, - "write": { + "lightness": { "overloads": [ [ - "String|Number|Array" + "p5.Color|Number[]|String" ] ] }, - "lightness": { + "imageMode": { "overloads": [ [ - "p5.Color|Number[]|String" + "CORNER|CORNERS|CENTER" ] ] }, - "imageMode": { + "imageShader": { "overloads": [ [ - "CORNER|CORNERS|CENTER" + "p5.Shader" ] ] }, @@ -2866,10 +2877,10 @@ ] ] }, - "imageShader": { + "write": { "overloads": [ [ - "p5.Shader" + "String|Number|Array" ] ] }, @@ -3078,15 +3089,6 @@ ] ] }, - "saveJSON": { - "overloads": [ - [ - "Array|Object", - "String", - "Boolean?" - ] - ] - }, "buildNormalShader": { "overloads": [ [ @@ -3106,6 +3108,15 @@ ] ] }, + "saveJSON": { + "overloads": [ + [ + "Array|Object", + "String", + "Boolean?" + ] + ] + }, "pop": { "overloads": [ [] @@ -3119,11 +3130,6 @@ ] ] }, - "requestPointerLock": { - "overloads": [ - [] - ] - }, "loadNormalShader": { "overloads": [ [ @@ -3133,6 +3139,11 @@ ] ] }, + "requestPointerLock": { + "overloads": [ + [] + ] + }, "baseNormalShader": { "overloads": [ [] @@ -3472,12 +3483,41 @@ ] ] }, + "createStorage": { + "overloads": [ + [ + "Number|Array|Float32Array|Object[]" + ] + ] + }, + "baseComputeShader": { + "overloads": [ + [] + ] + }, "metalness": { "overloads": [ [ "Number" ] ] + }, + "buildComputeShader": { + "overloads": [ + [ + "Function" + ] + ] + }, + "compute": { + "overloads": [ + [ + "p5.Shader", + "Number", + "Number?", + "Number?" + ] + ] } }, "p5.Element": { @@ -4193,44 +4233,6 @@ ] } }, - "p5.Shader": { - "version": { - "overloads": [ - [] - ] - }, - "inspectHooks": { - "overloads": [ - [] - ] - }, - "modify": { - "overloads": [ - [ - "Function", - "Object?" - ], - [ - "Object?" - ] - ] - }, - "copyToContext": { - "overloads": [ - [ - "p5|p5.Graphics" - ] - ] - }, - "setUniform": { - "overloads": [ - [ - "String", - "Boolean|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture" - ] - ] - } - }, "p5.Table": { "addRow": { "overloads": [ @@ -4405,6 +4407,44 @@ ] } }, + "p5.Shader": { + "version": { + "overloads": [ + [] + ] + }, + "inspectHooks": { + "overloads": [ + [] + ] + }, + "modify": { + "overloads": [ + [ + "Function", + "Object?" + ], + [ + "Object?" + ] + ] + }, + "copyToContext": { + "overloads": [ + [ + "p5|p5.Graphics" + ] + ] + }, + "setUniform": { + "overloads": [ + [ + "String", + "Boolean|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture|p5.StorageBuffer" + ] + ] + } + }, "p5.MediaElement": { "play": { "overloads": [ diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 59d58a590c..f51cfcc1d0 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -783,6 +783,39 @@ if (typeof p5 !== "undefined") { * See createFramebuffer. * * Note: The `getTexture` function is only available when using p5.strands. + */ + +/** + * Declares a storage buffer uniform inside a modify() callback, + * making a createStorage() buffer accessible in the shader. + * + * Pass a `p5.StorageBuffer` (or a function returning one) as the second argument + * to set it as the default value, applied automatically each frame. Pass a plain + * object with the same field layout as the buffer's struct elements to declare the + * schema without binding a specific buffer. + * + * When called without a name, p5.strands automatically uses the name of the + * variable it is assigned to as the uniform name. + * + * Note: `uniformStorage` is only available when using p5.strands. + * + * @method uniformStorage + * @beta + * @webgpu + * @submodule p5.strands + * @param {String} name The name of the storage buffer uniform in the shader. + * @param {p5.StorageBuffer|Function|Object} [bufferOrSchema] A storage buffer to bind, + * a function returning a storage buffer (called each frame), or a plain object + * describing the struct field layout. + * @returns {*} A strands node representing the storage buffer. + */ +/** + * @method uniformStorage + * @param {p5.StorageBuffer|Function|Object} [bufferOrSchema] + * @returns {*} + */ + +/** * * @method getTexture * @beta diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 7c9089fb9f..a56bcf4d1a 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -880,7 +880,7 @@ class Shader { * @chainable * @param {String} uniformName name of the uniform. Must match the name * used in the vertex and fragment shaders. - * @param {Boolean|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture} + * @param {Boolean|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture|p5.StorageBuffer} * data value to assign to the uniform. Must match the uniform’s data type. * * @example diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 59c863373e..b5ee50a688 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -34,6 +34,30 @@ function rendererWebGPU(p5, fn) { RGBA, } = p5; + class StorageBuffer { + constructor(buffer, size, renderer, schema = null) { + this._isStorageBuffer = true; + this.buffer = buffer; + this.size = size; + this._renderer = renderer; + this._schema = schema; + } + } + + /** + * Represents a GPU storage buffer created by createStorage(). + * + * Storage buffers hold data that can be read and written by compute shaders, + * and read by vertex and fragment shaders. Pass a `p5.StorageBuffer` to + * setUniform() or + * uniformStorage() to bind it to a shader. + * + * @class p5.StorageBuffer + * @beta + * @webgpu + */ + p5.StorageBuffer = StorageBuffer; + class RendererWebGPU extends Renderer3D { constructor(pInst, w, h, isMainCanvas, elt) { super(pInst, w, h, isMainCanvas, elt) @@ -3086,7 +3110,7 @@ ${hookUniformFields}} }); new Float32Array(buffer.getMappedRange()).set(packed); buffer.unmap(); - const storageBuffer = { _isStorageBuffer: true, buffer, size, _schema: schema, _renderer: this }; + const storageBuffer = new StorageBuffer(buffer, size, this, schema); this._storageBuffers.add(storageBuffer); return storageBuffer; } @@ -3126,14 +3150,7 @@ ${hookUniformFields}} buffer.unmap(); } - // Return wrapper object with metadata - const storageBuffer = { - _isStorageBuffer: true, - buffer, - size, - elementCount: size / 4, // Number of floats - _renderer: this - }; + const storageBuffer = new StorageBuffer(buffer, size, this); // Track for cleanup this._storageBuffers.add(storageBuffer); @@ -3633,9 +3650,11 @@ ${hookUniformFields}} * Creates a storage buffer for use in compute shaders. * * @method createStorage - * @param {Number|Array|Float32Array} dataOrCount Either a number specifying the count of floats, - * or an array/Float32Array with initial data. - * @returns {Object} A storage buffer object. + * @beta + * @webgpu + * @param {Number|Array|Float32Array|Object[]} dataOrCount Either a number specifying the count of floats, + * an array/Float32Array of floats, or an array of objects describing struct elements. + * @returns {p5.StorageBuffer} A storage buffer. */ fn.createStorage = function (dataOrCount) { return this._renderer.createStorage(dataOrCount); @@ -3650,18 +3669,132 @@ ${hookUniformFields}} * @method baseComputeShader * @submodule p5.strands * @beta + * @webgpu * @returns {p5.Shader} The base compute shader. */ fn.baseComputeShader = function () { return this._renderer.baseComputeShader(); }; + /** + * @property {Object} iteration + * @beta + * @webgpu + * @description + * Information about the current iteration of a compute shader. + + * Use it inside a + * `buildComputeShader()` + * function to write a loop that runs in parallel on the GPU. + * + * `iteration` has the following properties: + * - `index`: a three-component vector with the current index + * across all dimensions passed to + * `compute()`. For example, use + * `iteration.index.x` to get the index when looping in one dimension. + * - `localIndex`: an integer index of the thread within its workgroup. + * - `localId`: a three-component integer vector with the thread's position + * within its workgroup. + * - `workgroupId`: a three-component integer vector identifying which + * workgroup this thread belongs to. + */ + /** * Create a new compute shader using p5.strands. * + * A compute shader lets you run many calculations all at once on your GPU. They + * are similar to a `compute()` + * and passing the shader in, along with the number of iterations. + * + * ```js example + * let particles; + * let computeShader; + * let displayShader; + * let instance; + * const numParticles = 50; + * + * async function setup() { + * await createCanvas(100, 100, WEBGPU); + * + * let data = []; + * for (let i = 0; i < numParticles; i++) { + * data.push({ + * position: createVector( + * random(-40, 40), + * random(-40, 40) + * ), + * velocity: createVector( + * random(-1, 1), + * random(-1, 1) + * ), + * }); + * } + * particles = createStorage(data); + * + * computeShader = buildComputeShader( + * simulate, + * { particles } + * ); + * displayShader = buildMaterialShader( + * display, + * { particles } + * ); + * instance = buildGeometry(drawParticle); + * } + * + * function drawParticle() { + * sphere(3); + * } + * + * function simulate() { + * let r = 3; + * let particleData = uniformStorage(particles); + * let idx = iteration.index.x; + * let pos = particleData[idx].position; + * let vel = particleData[idx].velocity; + * pos = pos + vel; + * if (pos.x > width/2 - r || pos.x < -height/2 + r) { + * vel.x = -vel.x; + * pos.x = clamp(pos.x, -width/2 + r, width/2 - r); + * } + * if (pos.y > height/2 - r || pos.y < -height/2 + r) { + * vel.y = -vel.y; + * pos.y = clamp(pos.y, -height/2 + r, height/2 - r); + * } + * particleData[idx].position = pos; + * particleData[idx].velocity = vel; + * } + * + * function display() { + * let particleData = uniformStorage(particles); + * worldInputs.begin(); + * let pos = particleData[instanceID()].position; + * worldInputs.position.xy += pos; + * worldInputs.end(); + * } + * + * function draw() { + * background(30); + * compute(computeShader, numParticles); + * noStroke(); + * fill(255); + * lights(); + * shader(displayShader); + * model(geo, numParticles); + * } + * ``` + * * @method buildComputeShader * @submodule p5.strands * @beta + * @webgpu * @param {Function} callback A function building a p5.strands compute shader. * @returns {p5.Shader} The compute shader. */ @@ -3675,6 +3808,7 @@ ${hookUniformFields}} * @method compute * @submodule p5.strands * @beta + * @webgpu * @param {p5.Shader} shader The compute shader to run. * @param {Number} x Number of invocations in the X dimension. * @param {Number} [y=1] Number of invocations in the Y dimension. diff --git a/utils/data-processor.mjs b/utils/data-processor.mjs index 022e188792..6ebe805f8b 100644 --- a/utils/data-processor.mjs +++ b/utils/data-processor.mjs @@ -152,6 +152,7 @@ export function processData(rawData, strategy) { submodule, class: forEntry || 'p5', beta: entry.tags?.some(t => t.title === 'beta') || undefined, + webgpu: entry.tags?.some(t => t.title === 'webgpu') || undefined, }; processed.classitems.push(item); @@ -188,7 +189,9 @@ export function processData(rawData, strategy) { }, is_constructor: 1, module, - submodule + submodule, + beta: entry.tags?.some(t => t.title === 'beta') || undefined, + webgpu: entry.tags?.some(t => t.title === 'webgpu') || undefined, }; // The @private tag doesn't seem to end up in the Documentation.js output. @@ -269,6 +272,7 @@ export function processData(rawData, strategy) { module: prevItem?.module ?? module, submodule: prevItem?.submodule ?? submodule, beta: prevItem?.beta || entry.tags?.some(t => t.title === 'beta') || undefined, + webgpu: prevItem?.webgpu || entry.tags?.some(t => t.title === 'webgpu') || undefined, }; processed.classMethods[className] = processed.classMethods[className] || {}; From 276e84c3bc9cefa1a91250bcfcd0afb562385216 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 15 Mar 2026 15:13:17 -0400 Subject: [PATCH 067/250] Looks like the properties have to be at the bottom of the file so they dont cascade --- src/webgpu/p5.RendererWebGPU.js | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index b5ee50a688..7d62b8970e 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -3676,29 +3676,6 @@ ${hookUniformFields}} return this._renderer.baseComputeShader(); }; - /** - * @property {Object} iteration - * @beta - * @webgpu - * @description - * Information about the current iteration of a compute shader. - - * Use it inside a - * `buildComputeShader()` - * function to write a loop that runs in parallel on the GPU. - * - * `iteration` has the following properties: - * - `index`: a three-component vector with the current index - * across all dimensions passed to - * `compute()`. For example, use - * `iteration.index.x` to get the index when looping in one dimension. - * - `localIndex`: an integer index of the thread within its workgroup. - * - `localId`: a three-component integer vector with the thread's position - * within its workgroup. - * - `workgroupId`: a three-component integer vector identifying which - * workgroup this thread belongs to. - */ - /** * Create a new compute shader using p5.strands. * @@ -3817,6 +3794,29 @@ ${hookUniformFields}} fn.compute = function (shader, x, y, z) { this._renderer.compute(shader, x, y, z); }; + + /** + * Information about the current iteration of a compute shader. + + * Use it inside a + * `buildComputeShader()` + * function to write a loop that runs in parallel on the GPU. + * + * `iteration` has the following properties: + * - `index`: a three-component vector with the current index + * across all dimensions passed to + * `compute()`. For example, use + * `iteration.index.x` to get the index when looping in one dimension. + * - `localIndex`: an integer index of the thread within its workgroup. + * - `localId`: a three-component integer vector with the thread's position + * within its workgroup. + * - `workgroupId`: a three-component integer vector identifying which + * workgroup this thread belongs to. + * + * @property {Object} iteration + * @beta + * @webgpu + */ } export default rendererWebGPU; From 9de8e8f28dade3a63e60bc811bbd3003c544a02c Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 15 Mar 2026 15:24:25 -0400 Subject: [PATCH 068/250] Update modules in docs --- src/webgpu/p5.RendererWebGPU.js | 35 ++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 7d62b8970e..08d4c9a1ce 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -53,6 +53,8 @@ function rendererWebGPU(p5, fn) { * uniformStorage() to bind it to a shader. * * @class p5.StorageBuffer + * @module 3D + * @submodule p5.strands * @beta * @webgpu */ @@ -3652,6 +3654,8 @@ ${hookUniformFields}} * @method createStorage * @beta * @webgpu + * @module 3D + * @submodule p5.strands * @param {Number|Array|Float32Array|Object[]} dataOrCount Either a number specifying the count of floats, * an array/Float32Array of floats, or an array of objects describing struct elements. * @returns {p5.StorageBuffer} A storage buffer. @@ -3667,6 +3671,7 @@ ${hookUniformFields}} * calling `baseComputeShader().modify(shaderFunction)`. * * @method baseComputeShader + * @module 3D * @submodule p5.strands * @beta * @webgpu @@ -3715,14 +3720,8 @@ ${hookUniformFields}} * } * particles = createStorage(data); * - * computeShader = buildComputeShader( - * simulate, - * { particles } - * ); - * displayShader = buildMaterialShader( - * display, - * { particles } - * ); + * computeShader = buildComputeShader(simulate); + * displayShader = buildMaterialShader(display); * instance = buildGeometry(drawParticle); * } * @@ -3732,10 +3731,10 @@ ${hookUniformFields}} * * function simulate() { * let r = 3; - * let particleData = uniformStorage(particles); + * let data = uniformStorage(particles); * let idx = iteration.index.x; - * let pos = particleData[idx].position; - * let vel = particleData[idx].velocity; + * let pos = data[idx].position; + * let vel = data[idx].velocity; * pos = pos + vel; * if (pos.x > width/2 - r || pos.x < -height/2 + r) { * vel.x = -vel.x; @@ -3745,14 +3744,14 @@ ${hookUniformFields}} * vel.y = -vel.y; * pos.y = clamp(pos.y, -height/2 + r, height/2 - r); * } - * particleData[idx].position = pos; - * particleData[idx].velocity = vel; + * data[idx].position = pos; + * data[idx].velocity = vel; * } * * function display() { - * let particleData = uniformStorage(particles); + * let data = uniformStorage(particles); * worldInputs.begin(); - * let pos = particleData[instanceID()].position; + * let pos = data[instanceID()].position; * worldInputs.position.xy += pos; * worldInputs.end(); * } @@ -3764,11 +3763,12 @@ ${hookUniformFields}} * fill(255); * lights(); * shader(displayShader); - * model(geo, numParticles); + * model(instance, numParticles); * } * ``` * * @method buildComputeShader + * @module 3D * @submodule p5.strands * @beta * @webgpu @@ -3783,6 +3783,7 @@ ${hookUniformFields}} * Dispatches a compute shader to run on the GPU. * * @method compute + * @module 3D * @submodule p5.strands * @beta * @webgpu @@ -3816,6 +3817,8 @@ ${hookUniformFields}} * @property {Object} iteration * @beta * @webgpu + * @module 3D + * @submodule p5.strands */ } From 96a3f3c8a32035252e2632d5783f07446f304792 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 15 Mar 2026 15:27:29 -0400 Subject: [PATCH 069/250] Move module to the top of the file --- src/webgpu/p5.RendererWebGPU.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 08d4c9a1ce..4ea5e166f1 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1,3 +1,9 @@ +/** + * @module 3D + * @submodule p5.strands + * @for p5 + */ + import * as constants from '../core/constants'; import { getStrokeDefs } from '../webgl/enums'; import { DataType } from '../strands/ir_types.js'; @@ -53,8 +59,6 @@ function rendererWebGPU(p5, fn) { * uniformStorage() to bind it to a shader. * * @class p5.StorageBuffer - * @module 3D - * @submodule p5.strands * @beta * @webgpu */ @@ -3654,8 +3658,6 @@ ${hookUniformFields}} * @method createStorage * @beta * @webgpu - * @module 3D - * @submodule p5.strands * @param {Number|Array|Float32Array|Object[]} dataOrCount Either a number specifying the count of floats, * an array/Float32Array of floats, or an array of objects describing struct elements. * @returns {p5.StorageBuffer} A storage buffer. @@ -3671,8 +3673,6 @@ ${hookUniformFields}} * calling `baseComputeShader().modify(shaderFunction)`. * * @method baseComputeShader - * @module 3D - * @submodule p5.strands * @beta * @webgpu * @returns {p5.Shader} The base compute shader. @@ -3768,8 +3768,6 @@ ${hookUniformFields}} * ``` * * @method buildComputeShader - * @module 3D - * @submodule p5.strands * @beta * @webgpu * @param {Function} callback A function building a p5.strands compute shader. @@ -3783,8 +3781,6 @@ ${hookUniformFields}} * Dispatches a compute shader to run on the GPU. * * @method compute - * @module 3D - * @submodule p5.strands * @beta * @webgpu * @param {p5.Shader} shader The compute shader to run. @@ -3817,8 +3813,6 @@ ${hookUniformFields}} * @property {Object} iteration * @beta * @webgpu - * @module 3D - * @submodule p5.strands */ } From e3cf47fb9e1ba392c33b209b694732b35ab87653 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 15 Mar 2026 19:20:58 -0400 Subject: [PATCH 070/250] Add @webgpuOnly tag --- src/strands/p5.strands.js | 1 + src/webgpu/p5.RendererWebGPU.js | 6 ++++++ utils/data-processor.mjs | 3 +++ 3 files changed, 10 insertions(+) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index f51cfcc1d0..66def69183 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -802,6 +802,7 @@ if (typeof p5 !== "undefined") { * @method uniformStorage * @beta * @webgpu + * @webgpuOnly * @submodule p5.strands * @param {String} name The name of the storage buffer uniform in the shader. * @param {p5.StorageBuffer|Function|Object} [bufferOrSchema] A storage buffer to bind, diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 4ea5e166f1..8477465ba2 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -61,6 +61,7 @@ function rendererWebGPU(p5, fn) { * @class p5.StorageBuffer * @beta * @webgpu + * @webgpuOnly */ p5.StorageBuffer = StorageBuffer; @@ -3658,6 +3659,7 @@ ${hookUniformFields}} * @method createStorage * @beta * @webgpu + * @webgpuOnly * @param {Number|Array|Float32Array|Object[]} dataOrCount Either a number specifying the count of floats, * an array/Float32Array of floats, or an array of objects describing struct elements. * @returns {p5.StorageBuffer} A storage buffer. @@ -3675,6 +3677,7 @@ ${hookUniformFields}} * @method baseComputeShader * @beta * @webgpu + * @webgpuOnly * @returns {p5.Shader} The base compute shader. */ fn.baseComputeShader = function () { @@ -3770,6 +3773,7 @@ ${hookUniformFields}} * @method buildComputeShader * @beta * @webgpu + * @webgpuOnly * @param {Function} callback A function building a p5.strands compute shader. * @returns {p5.Shader} The compute shader. */ @@ -3783,6 +3787,7 @@ ${hookUniformFields}} * @method compute * @beta * @webgpu + * @webgpuOnly * @param {p5.Shader} shader The compute shader to run. * @param {Number} x Number of invocations in the X dimension. * @param {Number} [y=1] Number of invocations in the Y dimension. @@ -3813,6 +3818,7 @@ ${hookUniformFields}} * @property {Object} iteration * @beta * @webgpu + * @webgpuOnly */ } diff --git a/utils/data-processor.mjs b/utils/data-processor.mjs index 6ebe805f8b..7349e10773 100644 --- a/utils/data-processor.mjs +++ b/utils/data-processor.mjs @@ -153,6 +153,7 @@ export function processData(rawData, strategy) { class: forEntry || 'p5', beta: entry.tags?.some(t => t.title === 'beta') || undefined, webgpu: entry.tags?.some(t => t.title === 'webgpu') || undefined, + webgpuOnly: entry.tags?.some(t => t.title === 'webgpuOnly') || undefined, }; processed.classitems.push(item); @@ -192,6 +193,7 @@ export function processData(rawData, strategy) { submodule, beta: entry.tags?.some(t => t.title === 'beta') || undefined, webgpu: entry.tags?.some(t => t.title === 'webgpu') || undefined, + webgpuOnly: entry.tags?.some(t => t.title === 'webgpuOnly') || undefined, }; // The @private tag doesn't seem to end up in the Documentation.js output. @@ -273,6 +275,7 @@ export function processData(rawData, strategy) { submodule: prevItem?.submodule ?? submodule, beta: prevItem?.beta || entry.tags?.some(t => t.title === 'beta') || undefined, webgpu: prevItem?.webgpu || entry.tags?.some(t => t.title === 'webgpu') || undefined, + webgpuOnly: prevItem?.webgpuOnly || entry.tags?.some(t => t.title === 'webgpuOnly') || undefined, }; processed.classMethods[className] = processed.classMethods[className] || {}; From ea3b6b4a55160ba32794a93401e766a4a69db264 Mon Sep 17 00:00:00 2001 From: Kathrina Elangbam Date: Mon, 16 Mar 2026 10:03:00 +0530 Subject: [PATCH 071/250] Removed formatted code --- utils/patch.mjs | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/utils/patch.mjs b/utils/patch.mjs index 441b99d2a5..6845e072b4 100644 --- a/utils/patch.mjs +++ b/utils/patch.mjs @@ -3,18 +3,18 @@ import fs from 'fs'; export function applyPatches() { const cache = {}; const patched = {}; - + const replace = (path, src, dest) => { if (Array.isArray(path)) { path.forEach(path => replace(path, src, dest)); return; } try { - if (!path.startsWith('types/')) - path = 'types/' + path; + if (!path.startsWith("types/")) + path = "types/" + path; const before = patched[path] ?? - (cache[path] ??= fs.readFileSync('./' + path, { encoding: 'utf-8' })); + (cache[path] ??= fs.readFileSync("./" + path, { encoding: 'utf-8' })); const after = before.replaceAll(src, dest); if (after !== before) @@ -28,8 +28,8 @@ export function applyPatches() { // TODO: Handle this better in the docs instead of patching replace( - 'p5.d.ts', - 'constructor(detailX?: number, detailY?: number, callback?: Function);', + "p5.d.ts", + "constructor(detailX?: number, detailY?: number, callback?: Function);", `constructor( detailX?: number, detailY?: number, @@ -39,9 +39,9 @@ export function applyPatches() { // https://github.com/p5-types/p5.ts/issues/31 // #todo: add readonly to appropriate array params, either here or in doc comments replace( - ['p5.d.ts', 'global.d.ts'], - 'random(choices: any[]): any;', - 'random(choices: readonly T[]): T;' + ["p5.d.ts", "global.d.ts"], + "random(choices: any[]): any;", + "random(choices: readonly T[]): T;" ); replace( @@ -53,7 +53,7 @@ export function applyPatches() { replace( 'p5.d.ts', 'textToContours(str: string, x: number, y: number, options?: { sampleFactor?: number; simplifyThreshold?: number }): object[][];', - 'textToContours(str: string, x: number, y: number, options?: { sampleFactor?: number; simplifyThreshold?: number }): { x: number; y: number; alpha: number }[][];' + 'textToContours(str: string, x: number, y: number, options?: { sampleFactor?: number; simplifyThreshold?: number }): { x: number; y: number; alpha: number }[][];', ); replace( @@ -77,7 +77,7 @@ export function applyPatches() { 'class __Graphics extends p5.Element {', `class __Graphics extends p5.Element { elt: HTMLCanvasElement; - ` + `, ); // Type .elt more specifically for audio and video elements @@ -86,24 +86,24 @@ export function applyPatches() { `class MediaElement extends Element { elt: HTMLAudioElement | HTMLVideoElement;`, `class MediaElement extends Element { - elt: T;` + elt: T;`, ); replace( ['p5.d.ts', 'global.d.ts'], /createAudio\(src\?: string \| string\[\], callback\?: Function\): ([pP]5)\.MediaElement;/g, - 'createAudio(src?: string | string[], callback?: (video: $1.MediaElement) => any): $1.MediaElement;' + 'createAudio(src?: string | string[], callback?: (video: $1.MediaElement) => any): $1.MediaElement;', ); replace( ['p5.d.ts', 'global.d.ts'], /createVideo\(src\?: string \| string\[\], callback\?: Function\): ([pP]5)\.MediaElement;/g, - 'createVideo(src?: string | string[], callback?: (video: $1.MediaElement) => any): $1.MediaElement;' + 'createVideo(src?: string | string[], callback?: (video: $1.MediaElement) => any): $1.MediaElement;', ); // More callback types replace( ['p5.d.ts', 'global.d.ts'], /createFileInput\(callback: Function, multiple\?: boolean\): ([pP]5)\.Element;/g, - 'createFileInput(callback: (input: $1.File) => any, multiple?: boolean): $1.Element;' + 'createFileInput(callback: (input: $1.File) => any, multiple?: boolean): $1.Element;', ); replace( ['p5.d.ts', 'global.d.ts'], @@ -120,7 +120,7 @@ export function applyPatches() { replace( 'p5.d.ts', 'fontBounds(str: string, x: number, y: number, width?: number, height?: number): object;', - 'fontBounds(str: string, x: number, y: number, width?: number, height?: number): { x: number; y: number; w: number; h: number };' + 'fontBounds(str: string, x: number, y: number, width?: number, height?: number): { x: number; y: number; w: number; h: number };', ); replace( 'p5.d.ts', @@ -130,7 +130,7 @@ export function applyPatches() { replace( 'p5.d.ts', 'textBounds(str: string, x: number, y: number, width?: number, height?: number): object;', - 'textBounds(str: string, x: number, y: number, width?: number, height?: number): { x: number; y: number; w: number; h: number };' + 'textBounds(str: string, x: number, y: number, width?: number, height?: number): { x: number; y: number; w: number; h: number };', ); // Document Typr @@ -184,10 +184,9 @@ export function applyPatches() { for (const [path, data] of Object.entries(patched)) { try { console.log(`Patched ${path}`); - fs.writeFileSync('./' + path, data); + fs.writeFileSync("./" + path, data); } catch (err) { console.error(err); } } } - From 9de7f949717415ba23c7a2e11396efe9edb2baa9 Mon Sep 17 00:00:00 2001 From: Kathrina-dev Date: Mon, 16 Mar 2026 07:00:38 +0000 Subject: [PATCH 072/250] fix: typescript generation script for typedef constants --- src/strands/p5.strands.js | 13 +++++++- utils/patch.mjs | 15 ---------- utils/typescript.mjs | 63 +++++++++++++++++++++++++++++++-------- 3 files changed, 62 insertions(+), 29 deletions(-) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 73ca29c412..b78ab5a530 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -624,7 +624,18 @@ if (typeof p5 !== "undefined") { */ /** - * @property {Object} filterColor + * @typedef {Object} FilterColorHook + * @property {any} texCoord + * @property {any} canvasSize + * @property {any} texelSize + * @property {any} canvasContent + * @property {function(): undefined} begin + * @property {function(): undefined} end + * @property {function(color: any): void} set + */ + +/** + * @property {FilterColorHook} filterColor * @description * A shader hook block that sets the color for each pixel in a filter shader. This hook can be used inside `buildFilterShader()` to control the output color for each pixel. * diff --git a/utils/patch.mjs b/utils/patch.mjs index 6845e072b4..841ac9c703 100644 --- a/utils/patch.mjs +++ b/utils/patch.mjs @@ -166,21 +166,6 @@ export function applyPatches() { ` ); - // Fix filterColor hook typing - replace( - ['p5.d.ts'], - 'declare const filterColor: object;', - `declare const filterColor: { - texCoord: any; - canvasSize: any; - texelSize: any; - canvasContent: any; - begin(): void; - end(): void; - set(color: any): void; - };` - ); - for (const [path, data] of Object.entries(patched)) { try { console.log(`Patched ${path}`); diff --git a/utils/typescript.mjs b/utils/typescript.mjs index 88d5a93c21..4f2868aff4 100644 --- a/utils/typescript.mjs +++ b/utils/typescript.mjs @@ -26,10 +26,18 @@ const constantsLookup = new Set(); const typedefs = {}; const mutableProperties = new Set(['disableFriendlyErrors']); // Properties that should be mutable, not constants allRawData.forEach(entry => { - if (entry.kind === 'constant' || entry.kind === 'typedef') { + if (entry.kind === 'constant') { constantsLookup.add(entry.name); - if (entry.kind === 'typedef') { - typedefs[entry.name] = entry.type; + } + + // Collect object typedefs so constants referencing them can resolve to proper types + if (entry.kind === 'typedef') { + if ( + entry.properties && + entry.properties.length > 0 && + !entry.properties.every(p => p.name === entry.name) // exclude self-referential constants + ) { + typedefs[entry.name] = entry; } } }); @@ -201,12 +209,27 @@ function convertTypeToTypeScript(typeNode, options = {}) { throw new Error(`convertTypeToTypeScript expects an object, got: ${typeof typeNode} - ${JSON.stringify(typeNode)}`); } + if (typeNode.properties && typeNode.properties.length > 0) { + const props = typeNode.properties.map(prop => { + const propType = convertTypeToTypeScript(prop.type, options); + const optional = prop.optional ? '?' : ''; + return `${prop.name}${optional}: ${propType}`; + }); + + return `{ ${props.join('; ')} }`; + } + const { currentClass = null, isInsideNamespace = false, inGlobalMode = false, isConstantDef = false } = options; switch (typeNode.type) { case 'NameExpression': { const typeName = typeNode.name; + // Return typedef name directly so generated TS can reference the alias + if (Object.prototype.hasOwnProperty.call(typedefs, typeName)) { + return typeName; + } + // Handle primitive types const primitiveTypes = { 'String': 'string', @@ -595,17 +618,27 @@ function generateClassDeclaration(classData) { function generateTypeDefinitions() { let output = '// This file is auto-generated from JSDoc documentation\n\n'; + Object.entries(typedefs).forEach(([name, entry]) => { + if (entry.properties && entry.properties.length > 0) { + const props = entry.properties.map(prop => { + const propType = prop.type ? convertTypeToTypeScript(prop.type) : 'any'; + const optional = prop.optional ? '?' : ''; + return ` ${prop.name}${optional}: ${propType}`; + }); + output += `type ${name} = {\n${props.join(';\n')};\n};\n\n`; + } else { + const tsType = convertTypeToTypeScript(entry.type || entry); + output += `type ${name} = ${tsType};\n\n`; + } + }); + // First, define all constants at the top level with their actual values const seenConstants = new Set(); const p5Constants = processed.classitems.filter(item => { - if (item.class === 'p5' && item.itemtype === 'property' && item.name in processed.consts) { - // Skip defineProperty, undefined and avoid duplicates - if (item.name === 'defineProperty' || !item.name) { - return false; - } - if (seenConstants.has(item.name)) { - return false; - } + if (item.class === 'p5' && item.itemtype === 'property' && item.name in processed.consts) { + if (item.name === 'defineProperty' || !item.name) return false; + if (seenConstants.has(item.name)) return false; + if (item.name in typedefs) return false; // <-- add this line seenConstants.add(item.name); return true; } @@ -622,8 +655,12 @@ function generateTypeDefinitions() { const isMutable = mutableProperties.has(constant.name); const declaration = isMutable ? 'declare let' : 'declare const'; output += `${declaration} ${constant.name}: ${type};\n\n`; - // Duplicate with a private identifier so we can re-export in the namespace later - output += `${declaration} __${constant.name}: typeof ${constant.name};\n\n`; + + // Avoid __constant alias for typedef-backed hook objects + const isTypedefTyped = constant.type?.type === 'NameExpression' && constant.type?.name in typedefs; + if (!isTypedefTyped) { + output += `${declaration} __${constant.name}: typeof ${constant.name};\n\n`; + } }); // Generate main p5 class From fdf4b550b733c7a77fbeb11ce54d829206ea44ff Mon Sep 17 00:00:00 2001 From: Kathrina-dev Date: Mon, 16 Mar 2026 07:06:27 +0000 Subject: [PATCH 073/250] added condition for typedefs in p5Constants --- utils/typescript.mjs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/utils/typescript.mjs b/utils/typescript.mjs index 4f2868aff4..1e95464559 100644 --- a/utils/typescript.mjs +++ b/utils/typescript.mjs @@ -635,10 +635,17 @@ function generateTypeDefinitions() { // First, define all constants at the top level with their actual values const seenConstants = new Set(); const p5Constants = processed.classitems.filter(item => { - if (item.class === 'p5' && item.itemtype === 'property' && item.name in processed.consts) { - if (item.name === 'defineProperty' || !item.name) return false; - if (seenConstants.has(item.name)) return false; - if (item.name in typedefs) return false; // <-- add this line + if (item.class === 'p5' && item.itemtype === 'property' && item.name in processed.consts) { + // Skip defineProperty, undefined and avoid duplicates + if (item.name === 'defineProperty' || !item.name) { + return false; + } + if (seenConstants.has(item.name)) { + return false; + } + if (item.name in typedefs) { + return false; + } seenConstants.add(item.name); return true; } From 7a2524b225f355b4fac9b1fadf92b0682fb090dd Mon Sep 17 00:00:00 2001 From: Perminder Singh Date: Tue, 17 Mar 2026 01:40:05 +0530 Subject: [PATCH 074/250] harden github actions --- .github/workflows/auto-close-issues.yml | 10 +++++-- .github/workflows/ci-lint.yml | 14 ++++++---- .github/workflows/ci-test.yml | 18 ++++++++----- .github/workflows/contributors-png.yml | 11 +++++--- .github/workflows/labeler.yml | 3 ++- .github/workflows/release-workflow-v2.yml | 33 +++++++++++------------ .github/workflows/release-workflow.yml | 32 ++++++++++++---------- 7 files changed, 72 insertions(+), 49 deletions(-) diff --git a/.github/workflows/auto-close-issues.yml b/.github/workflows/auto-close-issues.yml index a2b3310aae..b993340d67 100644 --- a/.github/workflows/auto-close-issues.yml +++ b/.github/workflows/auto-close-issues.yml @@ -4,15 +4,21 @@ on: pull_request: types: [closed] branches: + - 2.0 - dev-2.0 +permissions: + contents: read + issues: write + pull-requests: read + jobs: close_issues: if: github.event.pull_request.merged == true runs-on: ubuntu-latest steps: - name: Close linked issues on non-default branches - uses: processing/branch-pr-close-issue@v1 + uses: processing/branch-pr-close-issue@9fd7b409a12c677c5cdd8ff82c45600f790074e1 # v1 with: token: ${{ secrets.GITHUB_TOKEN }} - branch: dev-2.0 + branch: dev-2.0 \ No newline at end of file diff --git a/.github/workflows/ci-lint.yml b/.github/workflows/ci-lint.yml index ea1eccbee2..37798a7d30 100644 --- a/.github/workflows/ci-lint.yml +++ b/.github/workflows/ci-lint.yml @@ -7,20 +7,24 @@ on: pull_request: branches: - '*' +permissions: + contents: read jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Use Node.js 22.x - uses: actions/setup-node@v1 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: - node-version: 22.x + persist-credentials: false + - name: Use Node.js 20.x + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 20.x - name: Get node modules run: npm ci env: CI: true - name: Lint source code - run: npm run lint + run: npm run lint \ No newline at end of file diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index e5ceb912c1..484f424510 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -9,6 +9,9 @@ on: branches: - '*' +permissions: + contents: read + jobs: test: strategy: @@ -22,10 +25,12 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false - name: Use Node.js 22.x - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22.x @@ -59,7 +64,7 @@ jobs: CI: true - name: Upload Visual Test Report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: visual-test-report path: test/unit/visual/visual-report.html @@ -74,9 +79,10 @@ jobs: CI: true - name: report test coverage if: steps.test.outcome == 'success' - run: bash <(curl -s https://codecov.io/bash) -f coverage/coverage-final.json - env: - CI: true + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + with: + files: coverage/coverage-final.json + fail_ci_if_error: false - name: fail job if tests failed if: steps.test.outcome != 'success' run: exit 1 \ No newline at end of file diff --git a/.github/workflows/contributors-png.yml b/.github/workflows/contributors-png.yml index 79933b44a4..321b5ddfa5 100644 --- a/.github/workflows/contributors-png.yml +++ b/.github/workflows/contributors-png.yml @@ -5,15 +5,20 @@ on: paths: - '.all-contributorsrc' +permissions: + contents: read + jobs: build: if: github.ref == 'refs/heads/main' && github.repository == 'processing/p5.js' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 @@ -30,7 +35,7 @@ jobs: git checkout -- . - name: Create Pull Request - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 with: commit-message: "Update contributors.png from .all-contributorsrc" branch: update-contributors-png diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 74e500b5e6..cc10da56c3 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -3,12 +3,13 @@ on: issues: types: [opened, edited] permissions: + contents: read issues: write jobs: triage: runs-on: ubuntu-latest steps: - - uses: github/issue-labeler@v3.2 + - uses: github/issue-labeler@98b5412841f6c4b0b3d9c29d53c13fad16bd7de2 # v3.2 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" configuration-path: .github/labeler.yml diff --git a/.github/workflows/release-workflow-v2.yml b/.github/workflows/release-workflow-v2.yml index 6574cc0e88..dff9a450b2 100644 --- a/.github/workflows/release-workflow-v2.yml +++ b/.github/workflows/release-workflow-v2.yml @@ -18,13 +18,15 @@ jobs: INPUT_TOKEN: ${{ secrets.NPM_TOKEN }} steps: # 1. Setup - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 with: - node-version: 22 + persist-credentials: false + - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3 + with: + node-version: 20 - name: Get semver info id: semver - uses: akshens/semver-tag@v4 + uses: akshens/semver-tag@8e427cd48c699c97d021df4946f3a0e65af5047e # v4 with: version: ${{ github.ref_name }} @@ -42,22 +44,16 @@ jobs: env: CI: true - name: Run test - run: npm test -- --project=unit-tests + run: npm test env: CI: true - name: Run build run: npm run build - - name: Generate types - run: npm run generate-types - - name: test TypeScript types - run: npm run test:types - env: - CI: true # 2. Prepare release files - run: mkdir release && mkdir p5 && cp -r ./lib/* p5/ - name: Create release zip file - uses: TheDoctor0/zip-release@0.6.2 + uses: TheDoctor0/zip-release@09336613be18a8208dfa66bd57efafd9e2685657 # 0.6.2 with: type: zip filename: release/p5.zip @@ -68,15 +64,15 @@ jobs: # 3. Release p5.js - name: Create GitHub release - uses: softprops/action-gh-release@v0.1.15 + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 with: draft: true prerelease: ${{ steps.semver.outputs.is-prerelease == 'true' }} files: release/* generate_release_notes: true - token: ${{ secrets.ACCESS_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} - name: Publish to NPM - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 with: token: ${{ secrets.NPM_TOKEN }} tag: ${{ steps.semver.outputs.is-prerelease != 'true' && 'latest' || 'beta' }} @@ -84,13 +80,14 @@ jobs: # 4. Update p5.js website - name: Clone p5.js website if: ${{ steps.semver.outputs.is-prerelease != 'true' }} - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 with: repository: processing/p5.js-website ref: '2.0' path: website fetch-depth: 0 token: ${{ secrets.ACCESS_TOKEN }} + persist-credentials: false - name: Updated website files if: ${{ steps.semver.outputs.is-prerelease != 'true' }} run: | @@ -111,9 +108,9 @@ jobs: git commit -m "Update p5.js to ${{ github.ref_name }}" - name: Push updated website repo if: ${{ steps.semver.outputs.is-prerelease != 'true' }} - uses: ad-m/github-push-action@v0.6.0 + uses: ad-m/github-push-action@40bf560936a8022e68a3c00e7d2abefaf01305a6 # v0.6.0 with: github_token: ${{ secrets.ACCESS_TOKEN }} branch: '2.0' directory: website/ - repository: processing/p5.js-website + repository: processing/p5.js-website \ No newline at end of file diff --git a/.github/workflows/release-workflow.yml b/.github/workflows/release-workflow.yml index 714f0890d0..72e5110f40 100644 --- a/.github/workflows/release-workflow.yml +++ b/.github/workflows/release-workflow.yml @@ -18,13 +18,15 @@ jobs: INPUT_TOKEN: ${{ secrets.NPM_TOKEN }} steps: # 1. Setup - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 with: - node-version: 22 + persist-credentials: false + - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3 + with: + node-version: 20 - name: Get semver info id: semver - uses: akshens/semver-tag@v4 + uses: akshens/semver-tag@8e427cd48c699c97d021df4946f3a0e65af5047e # v4 with: version: ${{ github.ref_name }} @@ -41,17 +43,16 @@ jobs: run: npm ci env: CI: true - - name: Run test + - name: Run build run: npm test env: CI: true - - name: Run build - run: npm run build + - run: rm ./lib/p5-test.js ./lib/p5.pre-min.js # 2. Prepare release files - run: mkdir release && mkdir p5 && cp -r ./lib/* p5/ - name: Create release zip file - uses: TheDoctor0/zip-release@0.6.2 + uses: TheDoctor0/zip-release@09336613be18a8208dfa66bd57efafd9e2685657 # 0.6.2 with: type: zip filename: release/p5.zip @@ -62,7 +63,7 @@ jobs: # 3. Release p5.js - name: Create GitHub release - uses: softprops/action-gh-release@v0.1.15 + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 with: draft: true prerelease: ${{ steps.semver.outputs.is-prerelease == 'true' }} @@ -71,19 +72,21 @@ jobs: token: ${{ secrets.ACCESS_TOKEN }} - name: Publish to NPM if: ${{ steps.semver.outputs.is-prerelease != 'true' }} - uses: JS-DevTools/npm-publish@v1 + uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 # v1 with: token: ${{ secrets.NPM_TOKEN }} + tag: r1 # 4. Update p5.js website - name: Clone p5.js website if: ${{ steps.semver.outputs.is-prerelease != 'true' }} - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 with: repository: processing/p5.js-website path: website fetch-depth: 0 token: ${{ secrets.ACCESS_TOKEN }} + persist-credentials: false - name: Updated website files if: ${{ steps.semver.outputs.is-prerelease != 'true' }} run: | @@ -104,7 +107,7 @@ jobs: git commit -m "Update p5.js to ${{ github.ref_name }}" - name: Push updated website repo if: ${{ steps.semver.outputs.is-prerelease != 'true' }} - uses: ad-m/github-push-action@v0.6.0 + uses: ad-m/github-push-action@40bf560936a8022e68a3c00e7d2abefaf01305a6 # v0.6.0 with: github_token: ${{ secrets.ACCESS_TOKEN }} branch: main @@ -114,12 +117,13 @@ jobs: # 5. Update Bower files - name: Checkout Bower repo if: ${{ steps.semver.outputs.is-prerelease != 'true' }} - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 with: repository: processing/p5.js-release path: bower fetch-depth: 0 token: ${{ secrets.ACCESS_TOKEN }} + persist-credentials: false - name: Copy new version files to Bower repo if: ${{ steps.semver.outputs.is-prerelease != 'true' }} run: | @@ -135,7 +139,7 @@ jobs: git commit -m "Update p5.js to ${{ github.ref_name }}" - name: Push updated Bower repo if: ${{ steps.semver.outputs.is-prerelease != 'true' }} - uses: ad-m/github-push-action@v0.6.0 + uses: ad-m/github-push-action@40bf560936a8022e68a3c00e7d2abefaf01305a6 # v0.6.0 with: github_token: ${{ secrets.ACCESS_TOKEN }} branch: master From dcd8ce3cb94464dbaab565f2da55304ccdae4717 Mon Sep 17 00:00:00 2001 From: Perminder Singh <127239756+perminder-17@users.noreply.github.com> Date: Tue, 17 Mar 2026 01:47:19 +0530 Subject: [PATCH 075/250] minor fixes --- .github/workflows/auto-close-issues.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/auto-close-issues.yml b/.github/workflows/auto-close-issues.yml index b993340d67..72ac018794 100644 --- a/.github/workflows/auto-close-issues.yml +++ b/.github/workflows/auto-close-issues.yml @@ -4,7 +4,6 @@ on: pull_request: types: [closed] branches: - - 2.0 - dev-2.0 permissions: @@ -21,4 +20,4 @@ jobs: uses: processing/branch-pr-close-issue@9fd7b409a12c677c5cdd8ff82c45600f790074e1 # v1 with: token: ${{ secrets.GITHUB_TOKEN }} - branch: dev-2.0 \ No newline at end of file + branch: dev-2.0 From 7b4f484aedd342cb8c358ac31b20be2d1a079648 Mon Sep 17 00:00:00 2001 From: Perminder Singh <127239756+perminder-17@users.noreply.github.com> Date: Tue, 17 Mar 2026 03:53:37 +0530 Subject: [PATCH 076/250] reverting unnecessary changes. --- .github/workflows/release-workflow-v2.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-workflow-v2.yml b/.github/workflows/release-workflow-v2.yml index dff9a450b2..216d6e7b9d 100644 --- a/.github/workflows/release-workflow-v2.yml +++ b/.github/workflows/release-workflow-v2.yml @@ -23,7 +23,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3 with: - node-version: 20 + node-version: 22 - name: Get semver info id: semver uses: akshens/semver-tag@8e427cd48c699c97d021df4946f3a0e65af5047e # v4 @@ -44,11 +44,17 @@ jobs: env: CI: true - name: Run test - run: npm test + run: npm test -- --project=unit-tests env: CI: true - name: Run build run: npm run build + - name: Generate types + run: npm run generate-types + - name: test TypeScript types + run: npm run test:types + env: + CI: true # 2. Prepare release files - run: mkdir release && mkdir p5 && cp -r ./lib/* p5/ @@ -113,4 +119,4 @@ jobs: github_token: ${{ secrets.ACCESS_TOKEN }} branch: '2.0' directory: website/ - repository: processing/p5.js-website \ No newline at end of file + repository: processing/p5.js-website From 8a3eedf899279ea839a241cb9d8329459ebdc900 Mon Sep 17 00:00:00 2001 From: Perminder Singh <127239756+perminder-17@users.noreply.github.com> Date: Tue, 17 Mar 2026 03:54:47 +0530 Subject: [PATCH 077/250] reverting unnecessary changes form release-workflow.yml --- .github/workflows/release-workflow.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release-workflow.yml b/.github/workflows/release-workflow.yml index 72e5110f40..40a944fa60 100644 --- a/.github/workflows/release-workflow.yml +++ b/.github/workflows/release-workflow.yml @@ -23,7 +23,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3 with: - node-version: 20 + node-version: 22 - name: Get semver info id: semver uses: akshens/semver-tag@8e427cd48c699c97d021df4946f3a0e65af5047e # v4 @@ -43,11 +43,12 @@ jobs: run: npm ci env: CI: true - - name: Run build + - name: Run test run: npm test env: CI: true - - run: rm ./lib/p5-test.js ./lib/p5.pre-min.js + - name: Run build + run: npm run build # 2. Prepare release files - run: mkdir release && mkdir p5 && cp -r ./lib/* p5/ @@ -69,13 +70,12 @@ jobs: prerelease: ${{ steps.semver.outputs.is-prerelease == 'true' }} files: release/* generate_release_notes: true - token: ${{ secrets.ACCESS_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} - name: Publish to NPM if: ${{ steps.semver.outputs.is-prerelease != 'true' }} uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 # v1 with: token: ${{ secrets.NPM_TOKEN }} - tag: r1 # 4. Update p5.js website - name: Clone p5.js website From a4c4bbd00f54d52b484d18e9dab86d1d5d479912 Mon Sep 17 00:00:00 2001 From: Perminder Singh <127239756+perminder-17@users.noreply.github.com> Date: Tue, 17 Mar 2026 03:56:04 +0530 Subject: [PATCH 078/250] reverting unnecessary changes from ci-lint.yml --- .github/workflows/ci-lint.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-lint.yml b/.github/workflows/ci-lint.yml index 37798a7d30..8f5091ec23 100644 --- a/.github/workflows/ci-lint.yml +++ b/.github/workflows/ci-lint.yml @@ -18,13 +18,13 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: persist-credentials: false - - name: Use Node.js 20.x + - name: Use Node.js 22.x uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: 20.x + node-version: 22.x - name: Get node modules run: npm ci env: CI: true - name: Lint source code - run: npm run lint \ No newline at end of file + run: npm run lint From c42ef81f05c01b6b4a506941db0fa050edc7dcc3 Mon Sep 17 00:00:00 2001 From: Kathrina-dev Date: Tue, 17 Mar 2026 16:49:45 +0000 Subject: [PATCH 079/250] fixed typescript JSDoc generator to handle unions --- utils/patch.mjs | 2 +- utils/typescript.mjs | 67 +++++++++++++++++++++++++++----------------- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/utils/patch.mjs b/utils/patch.mjs index 841ac9c703..dc59c005f6 100644 --- a/utils/patch.mjs +++ b/utils/patch.mjs @@ -174,4 +174,4 @@ export function applyPatches() { console.error(err); } } -} +} \ No newline at end of file diff --git a/utils/typescript.mjs b/utils/typescript.mjs index 685bc0301f..6fb6d3037c 100644 --- a/utils/typescript.mjs +++ b/utils/typescript.mjs @@ -225,11 +225,6 @@ function convertTypeToTypeScript(typeNode, options = {}) { case 'NameExpression': { const typeName = typeNode.name; - // Return typedef name directly so generated TS can reference the alias - if (Object.prototype.hasOwnProperty.call(typedefs, typeName)) { - return typeName; - } - // Handle primitive types const primitiveTypes = { 'String': 'string', @@ -265,19 +260,18 @@ function convertTypeToTypeScript(typeNode, options = {}) { } } - // Check if this is a p5 constant - use typeof since they're defined as values + // If constant: use its typedef when defining it, else reference it as a value via `typeof` if (constantsLookup.has(typeName)) { - if (inGlobalMode) { - return `typeof P5.${typeName}`; - } else if (typedefs[typeName]) { - if (isConstantDef) { - return convertTypeToTypeScript(typedefs[typeName], options); - } else { - return `typeof p5.${typeName}` - } - } else { - return `Symbol`; + if (isConstantDef && typedefs[typeName]) { + return convertTypeToTypeScript(typedefs[typeName], options); } + return inGlobalMode + ? `typeof P5.${typeName}` + : `typeof ${typeName}`; + } + + if (typedefs[typeName]) { + return typeName; } return typeName; @@ -391,6 +385,11 @@ const typescriptStrategy = { const processed = processData(rawData, typescriptStrategy); +// Augment constantsLookup with processed constants +Object.keys(processed.consts).forEach(name => { + constantsLookup.add(name); +}); + function normalizeIdentifier(name) { return ( '0123456789'.includes(name[0]) || @@ -618,6 +617,8 @@ function generateClassDeclaration(classData) { function generateTypeDefinitions() { let output = '// This file is auto-generated from JSDoc documentation\n\n'; + const strandsMethods = processStrandsFunctions(); + Object.entries(typedefs).forEach(([name, entry]) => { if (entry.properties && entry.properties.length > 0) { const props = entry.properties.map(prop => { @@ -658,16 +659,26 @@ function generateTypeDefinitions() { output += formatJSDocComment(constant.description, 0) + '\n'; output += ' */\n'; } - const type = convertTypeToTypeScript(constant.type, { isInsideNamespace: false, isConstantDef: true }); + let type; + // Avoid invalid self-referential types like `typeof FOO` + if ( + constant.type?.type === 'NameExpression' && + constant.type.name === constant.name + ) { + // Self-referential constant → fallback + type = 'number'; + } else { + type = convertTypeToTypeScript(constant.type, { + isInsideNamespace: false, + isConstantDef: true + }); + } const isMutable = mutableProperties.has(constant.name); const declaration = isMutable ? 'declare let' : 'declare const'; output += `${declaration} ${constant.name}: ${type};\n\n`; - // Avoid __constant alias for typedef-backed hook objects - const isTypedefTyped = constant.type?.type === 'NameExpression' && constant.type?.name in typedefs; - if (!isTypedefTyped) { - output += `${declaration} __${constant.name}: typeof ${constant.name};\n\n`; - } + output += `${declaration} __${constant.name}: typeof ${constant.name};\n\n`; + }); // Generate main p5 class @@ -689,7 +700,6 @@ function generateTypeDefinitions() { }); // Add strands functions to p5 instance - const strandsMethods = processStrandsFunctions(); strandsMethods.forEach(method => { output += generateMethodDeclaration(method, p5Options); }); @@ -711,9 +721,16 @@ function generateTypeDefinitions() { output += '\n'; - p5Constants.forEach(constant => { - output += `${mutableProperties.has(constant.name) ? 'let' : 'const'} ${constant.name}: typeof __${constant.name};\n`; + const isTypedefTyped = + constant.type?.type === 'NameExpression' && + constant.type?.name in typedefs; + + if (isTypedefTyped) { + output += `${mutableProperties.has(constant.name) ? 'let' : 'const'} ${constant.name}: ${constant.type.name};\n`; + } else { + output += `${mutableProperties.has(constant.name) ? 'let' : 'const'} ${constant.name}: typeof __${constant.name};\n`; + } }); output += '\n'; From c809894327a3ebf387da41a301859e4cdd0415ed Mon Sep 17 00:00:00 2001 From: Kathrina Elangbam Date: Tue, 17 Mar 2026 22:23:14 +0530 Subject: [PATCH 080/250] removed auto-formatting --- utils/patch.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/patch.mjs b/utils/patch.mjs index dc59c005f6..841ac9c703 100644 --- a/utils/patch.mjs +++ b/utils/patch.mjs @@ -174,4 +174,4 @@ export function applyPatches() { console.error(err); } } -} \ No newline at end of file +} From 51a03f0a71b7a8716a390d8588bbb0e6a03b7bbc Mon Sep 17 00:00:00 2001 From: Kathrina Elangbam Date: Tue, 17 Mar 2026 22:24:47 +0530 Subject: [PATCH 081/250] added useful comments --- utils/typescript.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/typescript.mjs b/utils/typescript.mjs index 6fb6d3037c..66c32e1f24 100644 --- a/utils/typescript.mjs +++ b/utils/typescript.mjs @@ -260,7 +260,7 @@ function convertTypeToTypeScript(typeNode, options = {}) { } } - // If constant: use its typedef when defining it, else reference it as a value via `typeof` + // If p5 constant: use its typedef when defining it, else reference it as a value via `typeof` if (constantsLookup.has(typeName)) { if (isConstantDef && typedefs[typeName]) { return convertTypeToTypeScript(typedefs[typeName], options); @@ -881,4 +881,4 @@ console.log('TypeScript definitions generated successfully!'); // Apply patches console.log('Applying TypeScript patches...'); -applyPatches(); \ No newline at end of file +applyPatches(); From ca4c1a45ac6a65ba59a97fcbb70d11e01353a79c Mon Sep 17 00:00:00 2001 From: Kathrina Elangbam Date: Tue, 17 Mar 2026 22:30:55 +0530 Subject: [PATCH 082/250] added useful comments --- utils/typescript.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/typescript.mjs b/utils/typescript.mjs index 66c32e1f24..9ee9f2352e 100644 --- a/utils/typescript.mjs +++ b/utils/typescript.mjs @@ -35,7 +35,7 @@ allRawData.forEach(entry => { if ( entry.properties && entry.properties.length > 0 && - !entry.properties.every(p => p.name === entry.name) // exclude self-referential constants + !entry.properties.every(p => p.name === entry.name) ) { typedefs[entry.name] = entry; } @@ -676,7 +676,7 @@ function generateTypeDefinitions() { const isMutable = mutableProperties.has(constant.name); const declaration = isMutable ? 'declare let' : 'declare const'; output += `${declaration} ${constant.name}: ${type};\n\n`; - + // Duplicate with a private identifier so we can re-export in the namespace later output += `${declaration} __${constant.name}: typeof ${constant.name};\n\n`; }); From 16381a180493ed08f5effec2eaec24d00c0e283a Mon Sep 17 00:00:00 2001 From: Rizky Mirzaviandy Priambodo <142987522+Xavrir@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:36:46 +0700 Subject: [PATCH 083/250] docs: add iframe accessible name guidance to style guide --- contributor_docs/documentation_style_guide.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/contributor_docs/documentation_style_guide.md b/contributor_docs/documentation_style_guide.md index d2299b27ab..29eac1c038 100644 --- a/contributor_docs/documentation_style_guide.md +++ b/contributor_docs/documentation_style_guide.md @@ -26,6 +26,7 @@ Our community is large and diverse. Many people learn to code using p5.js, and a - [Code Samples](#code-samples) - [Comments](#comments) - [Accessible Canvas Labels](#accessible-canvas-labels) +- [Accessible Iframe Names](#accessible-iframe-names) - [Whitespace](#whitespace) - [Semicolons](#semicolons) - [Naming Conventions](#naming-conventions) @@ -273,6 +274,27 @@ The above examples and suggestions are based on the [Writing Accessible Canvas D To understand the structure of p5.js’ web accessibility features for contributors, see the [Web Accessibility Contributor Doc](./web_accessibility.md#user-generated-accessible-canvas-descriptions). +**[⬆ back to top](#table-of-contents)** + +## Accessible Iframe Names + +- When embedding content with ` + + + +``` + +- Use a `title` that describes what the iframe contains, not just "iframe" or "embedded content." + +- If the iframe is purely decorative and carries no meaningful content, use `aria-hidden="true"` instead. + + **[⬆ back to top](#table-of-contents)** ## Whitespace From 7963a6e6c090b1a15f1224b1d91f331587efd7bd Mon Sep 17 00:00:00 2001 From: Rizky Mirzaviandy Priambodo <142987522+Xavrir@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:37:33 +0700 Subject: [PATCH 084/250] docs: fix typos and grammar in contributor docs --- contributor_docs/contributing_to_the_p5js_reference.md | 2 +- contributor_docs/documentation_style_guide.md | 2 +- contributor_docs/project_wrapups/katiejliu_gsoc_2021.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contributor_docs/contributing_to_the_p5js_reference.md b/contributor_docs/contributing_to_the_p5js_reference.md index eaa8750865..0bd05a209d 100644 --- a/contributor_docs/contributing_to_the_p5js_reference.md +++ b/contributor_docs/contributing_to_the_p5js_reference.md @@ -1190,7 +1190,7 @@ In some editors, such as vs code, you can hover over a function or variable to s ### Previewing your work on the website, locally -At some point you will want to preview how your changes will look on the website. This involves run the website locally and having it import your p5.js code from a branch of your repo. +At some point you will want to preview how your changes will look on the website. This involves running the website locally and having it import your p5.js code from a branch of your repo. Steps: diff --git a/contributor_docs/documentation_style_guide.md b/contributor_docs/documentation_style_guide.md index d2299b27ab..1710150907 100644 --- a/contributor_docs/documentation_style_guide.md +++ b/contributor_docs/documentation_style_guide.md @@ -234,7 +234,7 @@ let magicWord = 'Please'; ## Accessible Canvas Labels -- Use `describe()` to in p5.js example code, to add labels to your canvas so that it’s readable for screen readers. +- Use `describe()` in p5.js example code to add labels to your canvas so that it’s readable for screen readers. > Why? It makes examples accessible to screen readers, and models how to write good canvas labels. diff --git a/contributor_docs/project_wrapups/katiejliu_gsoc_2021.md b/contributor_docs/project_wrapups/katiejliu_gsoc_2021.md index 750600bd46..7c42909474 100644 --- a/contributor_docs/project_wrapups/katiejliu_gsoc_2021.md +++ b/contributor_docs/project_wrapups/katiejliu_gsoc_2021.md @@ -3,7 +3,7 @@ #### Mentors: Rachel Lim and Claire Kearney-Volpe ### Overview - For my Google Summer of Code project, I added alt text to the visual elements of the p5.js website, specfically to all of the examples. With the help of my mentors Rachel and Claire, I was able to improve the accessibility of the p5.js website for users with visually impairment. + For my Google Summer of Code project, I added alt text to the visual elements of the p5.js website, specifically to all of the examples. With the help of my mentors Rachel and Claire, I was able to improve the accessibility of the p5.js website for users with visually impairment. ### Process To begin, I did a lot of research on best alt text practices. I read about web accessibility through sources such as articles from WebAIM and the Web Content Accessibility Guidelines from W3C. I also familiarized myself with navigating and using the built-in screen reader on my Mac. From bea47ea3b35b9a54833101e9ad2a8789750b0820 Mon Sep 17 00:00:00 2001 From: VANSH3104 Date: Wed, 18 Mar 2026 18:15:51 +0530 Subject: [PATCH 085/250] Fix: Initialize shape with vertex properties in arc() to match ellipse() pattern --- src/core/p5.Renderer2D.js | 61 ++++++++------------------- src/shape/custom_shapes.js | 85 +++++++++++++++++++++++++++++++------- 2 files changed, 87 insertions(+), 59 deletions(-) diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 206d187f5f..981ae3288d 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -8,7 +8,7 @@ import { MediaElement } from '../dom/p5.MediaElement'; import { RGBHDR } from '../color/creating_reading'; import FilterRenderer2D from '../image/filterRenderer2D'; import { Matrix } from '../math/p5.Matrix'; -import { PrimitiveToPath2DConverter, ArcPrimitive, EllipsePrimitive } from '../shape/custom_shapes'; +import { PrimitiveToPath2DConverter } from '../shape/custom_shapes'; import { DefaultFill, textCoreConstants } from '../type/textCore'; @@ -661,22 +661,18 @@ class Renderer2D extends Renderer { * start <= stop < start + TWO_PI */ arc(x, y, w, h, start, stop, mode) { - 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; - } - - const primitive = new ArcPrimitive(x, y, w, h, start, stop, mode); - const shape = { accept(visitor) { primitive.accept(visitor); } }; + const shape = new p5.Shape({ position: new p5.Vector(0, 0) }); + shape.beginShape(); + shape.arcPrimitive( + x, + y, + w, + h, + start, + stop, + mode + ); + shape.endShape(); this.drawShape(shape); return this; @@ -684,39 +680,16 @@ class Renderer2D extends Renderer { } ellipse(args) { - 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; - } - const primitive = new EllipsePrimitive(x, y, w, h); - const shape = { accept(visitor) { primitive.accept(visitor); } }; + const shape = new p5.Shape({ position: new p5.Vector(0, 0) }); + shape.beginShape(); + shape.ellipsePrimitive(x,y,w,h); + shape.endShape(); this.drawShape(shape); - return this; } diff --git a/src/shape/custom_shapes.js b/src/shape/custom_shapes.js index 61ffe2ba4c..f100808803 100644 --- a/src/shape/custom_shapes.js +++ b/src/shape/custom_shapes.js @@ -474,12 +474,11 @@ class ArcPrimitive extends ShapePrimitive { #start; #stop; #mode; - // vertexCapacity 0 means this primitive should not accumulate normal path vertices - #vertexCapacity = 0; + #vertexCapacity = 2; - constructor(x, y, w, h, start, stop, mode) { + constructor(startVertex, endVertex, x, y, w, h, start, stop, mode) { // ShapePrimitive requires at least one vertex; pass a placeholder - super(new Vertex({ position: new Vector(x + w / 2, y + h / 2) })); + super(startVertex, endVertex); this.#x = x; this.#y = y; this.#w = w; @@ -511,12 +510,11 @@ class EllipsePrimitive extends ShapePrimitive { #y; #w; #h; - // vertexCapacity 0 means this primitive should not accumulate normal path vertices - #vertexCapacity = 0; + #vertexCapacity = 1; - constructor(x, y, w, h) { + constructor(centerVertex, x, y, w, h) { - super(new Vertex({ position: new Vector(x + w / 2, y + h / 2) })); + super(centerVertex); this.#x = x; this.#y = y; this.#w = w; @@ -976,6 +974,44 @@ class Shape { this.#generalVertex('arcVertex', position, textureCoordinates); } + + arcPrimitive(x,y,w,h,start,stop,mode){ + const centerX = x+w/2; + const centerY = y+h/2; + + const startVertex = this.#createVertex( + new Vector( + centerX+(w/2)*Math.cos(start), + centerY+(h/2)*Math.sin(start) + ) + ); + + const endVertex = this.#createVertex( + new Vector( + centerX+(w/2)*Math.cos(stop), + centerY+(h/2)*Math.sin(stop) + ) + ); + + const primitive = new ArcPrimitive( + startVertex, + endVertex, + x, y, w, h, + start, + stop, + mode + ); + return primitive.addToShape(this); + + } + + ellipsePrimitive(x,y,w,h){ + const centerVertex = this.#createVertex(new Vector(x+w/2,y+h/2)); + + const primitive = new EllipsePrimitive(centerVertex, x, y, w, h); + return primitive.addToShape(this); + } + beginContour(shapeKind = constants.PATH) { if (this.at(-1)?.kind === constants.EMPTY_PATH) { this.contours.pop(); @@ -1387,7 +1423,11 @@ class PrimitiveToVerticesConverter extends PrimitiveVisitor { const centerY = arc.y + arc.h / 2; const radiusX = arc.w / 2; const radiusY = arc.h / 2; - const numPoints = Math.max(3, this.curveDetail); + const avgRadius = (radiusX+radiusY)/2; + + const arcLength = avgRadius*Math.abs(arc.stop-arc.start); + + const numPoints=Math.max(3, Math.ceil(this.curveDetail*arcLength)); const verts = []; if (arc.mode === constants.PIE) { @@ -1396,12 +1436,27 @@ class PrimitiveToVerticesConverter extends PrimitiveVisitor { for (let i = 0; i <= numPoints; i++) { const angle = arc.start + (arc.stop - arc.start) * (i / numPoints); - verts.push(new Vertex({ - position: new Vector( - centerX + radiusX * Math.cos(angle), - centerY + radiusY * Math.sin(angle) - ) - })); + const startVertex=arc.vertices[0]; + const endVertex=arc.vertices[1]; + const t=i/numPoints; + const props={}; + for(const key in startVertex){ + if(key === 'position') continue; + if(typeof startVertex[key] === 'number' + && typeof endVertex[key]=== 'number'){ + props[key] = startVertex[key]*(1-t) + endVertex[key]*t; + } + else{ + props[key]=startVertex[key]; + } + } + + props.position=new Vector( + centerX+radiusX*Math.cos(angle), + centerY+radiusY*Math.sin(angle) + ); + + verts.push(new Vertex(props)); } this.contours.push(verts); From a84505e6eefdde1de551370da81090c2f35a9858 Mon Sep 17 00:00:00 2001 From: kit Date: Wed, 18 Mar 2026 13:04:36 +0100 Subject: [PATCH 086/250] shared abstraction for vector validation --- src/math/math.js | 12 ++++++- src/math/p5.Vector.js | 20 ++++++++---- src/math/patch-vector.js | 65 ++++++++++++++++++++++--------------- src/webgl/loading.js | 2 +- src/webgl/p5.Camera.js | 8 ++--- src/webgl/p5.Geometry.js | 2 +- test/unit/math/p5.Vector.js | 16 ++++----- 7 files changed, 77 insertions(+), 48 deletions(-) diff --git a/src/math/math.js b/src/math/math.js index 89c44956a1..4371585b96 100644 --- a/src/math/math.js +++ b/src/math/math.js @@ -96,7 +96,17 @@ function math(p5, fn) { * } */ fn.createVector = function (...args) { - return new p5.Vector(...args); + + // TODO MOVE PASSING THESE FUNCTIONS TO VECTOR + if (this instanceof p5) { + return new p5.Vector( + this._fromRadians.bind(this), + this._toRadians.bind(this), + ...arguments + ); + } else { + return new p5.Vector(...args); + } }; /** diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index f0b9899792..45a8d9623f 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -9,7 +9,8 @@ import * as constants from '../core/constants'; * This function is used by binary vector operations to prioritize shorter vectors, * and to emit a warning when lengths do not match. */ -const smallerDimensionPriority = function(dimOther, dimSelf) { +const smallerDimensionPriority = function(dimOther, dimSelf, args) { + console.log("sDP", args); const minDimension = Math.min(dimOther, dimSelf); if (dimOther !== dimSelf) { console.warn( @@ -39,6 +40,11 @@ class Vector { // This is how it comes in with createVector() // This check if the first argument is a function constructor(...args) { + + + // not meant to be userfacing so requires valid input + // TODO throw error when no input args + if (typeof args[0] === 'function') { this.isPInst = true; this._fromRadians = args[0]; @@ -46,6 +52,8 @@ class Vector { args = args.slice(2); } + // todo at this point must be numbers + this.values = args; } @@ -462,7 +470,7 @@ class Vector { * @chainable */ add(...args) { - const minDimension = smallerDimensionPriority(args.length, this.dimensions); + const minDimension = smallerDimensionPriority(args.length, this.dimensions, args); this.values = this.values.reduce((acc, v, i) => { if(i < minDimension) acc[i] = this.values[i] + Number(args[i]); @@ -582,7 +590,7 @@ class Vector { * @chainable */ rem(...args) { - const minDimension = smallerDimensionPriority(args.length, this.dimensions); + const minDimension = smallerDimensionPriority(args.length, this.dimensions, args); this.values = Array.from({ length: minDimension }, (_, i) => { return (args[i] > 0) ? this.values[i] % args[i] : this.values[i]; @@ -711,7 +719,7 @@ class Vector { * @chainable */ sub(...args) { - const minDimension = smallerDimensionPriority(args.length, this.dimensions); + const minDimension = smallerDimensionPriority(args.length, this.dimensions, args); this.values = this.values.reduce((acc, v, i) => { if(i < minDimension) acc[i] = this.values[i] - args[i]; @@ -896,7 +904,7 @@ class Vector { * @chainable */ mult(...args) { - const minDimension = smallerDimensionPriority(args.length, this.dimensions); + const minDimension = smallerDimensionPriority(args.length, this.dimensions, args); this.values = this.values.reduce((acc, v, i) => { if(i < minDimension) acc[i] = this.values[i] * args[i]; @@ -1120,7 +1128,7 @@ class Vector { * @chainable */ div(...args) { - const minDimension = smallerDimensionPriority(args.length, this.dimensions); + const minDimension = smallerDimensionPriority(args.length, this.dimensions, args); if(!args.every(v => typeof v === 'number' && v !== 0)){ console.warn( diff --git a/src/math/patch-vector.js b/src/math/patch-vector.js index 075b0aa5e5..cd5b8968e9 100644 --- a/src/math/patch-vector.js +++ b/src/math/patch-vector.js @@ -13,6 +13,9 @@ export function _defaultEmptyVector(target){ ); return target.call(this, 0, 0, 0); }else{ + if (Array.isArray(args[0])) { + args = args[0]; + } return target.call(this, ...args); } }; @@ -23,29 +26,37 @@ export function _defaultEmptyVector(target){ * @private * @internal */ -export function _validatedVectorOperation(target){ - return function(...args){ - if (args.length === 0) { - // No arguments? No action - return this; - } else if (args[0] instanceof Vector) { - args = args[0].values; - } else if (Array.isArray(args[0])) { - args = args[0]; - } else if (args.length === 1) { - // Solo argument? This is a special case - args = new Array(3).fill(args[0]); - } +export function _validatedVectorOperation(expectsSoloNumberArgument){ + return function(target){ + return function(...args){ + console.log("vVO", target.name, args); + if (args.length === 0) { + // No arguments? No action + return this; + } else if (args[0] instanceof Vector) { + args = args[0].values; + } else if (Array.isArray(args[0])) { + args = args[0]; + } else if (args.length === 1) { + console.log("A") + if (expectsSoloNumberArgument){ + console.log("b") + // && typeof args[0] === 'number' && Number.isFinite(args[0]) + // Special case handling for a solo numeric argument + args = new Array(3).fill(args[0]); + } + } // (1,2,3) ...args is 1,2,3 - if(!args.every(v => typeof v === 'number' && Number.isFinite(v))){ - p5._friendlyError( - 'Arguments contain non-finite numbers', - target.name - ); - return this; - }; + if(Array.isArray(args) && !args.every(v => typeof v === 'number' && Number.isFinite(v))){ + p5._friendlyError( + 'Arguments contain non-finite numbers', + target.name + ); + return this; + }; - return target.call(this, ...args); + return target.call(this, ...args); + }; }; } @@ -57,12 +68,14 @@ export function _validatedVectorOperation(target){ export default function vectorValidation(p5, fn, lifecycles){ p5.registerDecorator('p5.prototype.createVector', _defaultEmptyVector); + p5.registerDecorator('p5.Vector.prototype.mult', _validatedVectorOperation(true)); - p5.registerDecorator('p5.Vector.prototype.add', _validatedVectorOperation); - p5.registerDecorator('p5.Vector.prototype.sub', _validatedVectorOperation); - p5.registerDecorator('p5.Vector.prototype.mult', _validatedVectorOperation); + p5.registerDecorator(function(path){ + return ['p5.Vector.prototype.add', 'p5.Vector.prototype.sub'].includes(path); + }, _validatedVectorOperation(false)); - p5.registerDecorator('p5.Vector.prototype.rem', _validatedVectorOperation); - p5.registerDecorator('p5.Vector.prototype.div', _validatedVectorOperation); + p5.registerDecorator(function(path){ + return ['p5.Vector.prototype.rem', 'p5.Vector.prototype.div'].includes(path); + }, _validatedVectorOperation(true)); } diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 1e804a4c4a..d607fb4092 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -609,7 +609,7 @@ function loading(p5, fn){ model.uvs.push(loadedVerts.vt.at(vertParts[1]) ? loadedVerts.vt.at(vertParts[1]).slice() : [0, 0]); model.vertexNormals.push(loadedVerts.vn.at(vertParts[2]) ? - loadedVerts.vn.at(vertParts[2]).copy() : new Vector()); + loadedVerts.vn.at(vertParts[2]).copy() : new Vector(0, 0, 0)); usedVerts[vertString][currentMaterial] = vertIndex; face.push(vertIndex); diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js index 5dcca97a20..b4dbece928 100644 --- a/src/webgl/p5.Camera.js +++ b/src/webgl/p5.Camera.js @@ -1601,10 +1601,10 @@ class Camera { const up1 = rotMat1.row(1); // prepare new vectors. - const newFront = new Vector(); - const newUp = new Vector(); - const newEye = new Vector(); - const newCenter = new Vector(); + const newFront = new Vector(0, 0, 0); + const newUp = new Vector(0, 0, 0); + const newEye = new Vector(0, 0, 0); + const newCenter = new Vector(0, 0, 0); // Create the inverse matrix of mat0 by transposing mat0, // and multiply it to mat1 from the right. diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index dad4c860f4..5da0ddbb10 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -1205,7 +1205,7 @@ class Geometry { // initialize the vertexNormals array with empty vectors vertexNormals.length = 0; for (iv = 0; iv < vertices.length; ++iv) { - vertexNormals.push(new Vector()); + vertexNormals.push(new Vector(0, 0, 0)); } // loop through all the faces adding its normal to the normal diff --git a/test/unit/math/p5.Vector.js b/test/unit/math/p5.Vector.js index a28717be49..537ff1a110 100644 --- a/test/unit/math/p5.Vector.js +++ b/test/unit/math/p5.Vector.js @@ -23,9 +23,9 @@ suite('p5.Vector', function () { // The following mocks simulate the validation decorator Vector.prototype.add = _validatedVectorOperation(Vector.prototype.add); Vector.prototype.sub = _validatedVectorOperation(Vector.prototype.sub); - Vector.prototype.mult = _validatedVectorOperation(Vector.prototype.mult); - Vector.prototype.rem = _validatedVectorOperation(Vector.prototype.rem); - Vector.prototype.div = _validatedVectorOperation(Vector.prototype.div); + Vector.prototype.mult = _validatedVectorOperationtk(Vector.prototype.mult); + Vector.prototype.rem = _validatedVectorOperationtk(Vector.prototype.rem); + Vector.prototype.div = _validatedVectorOperationjt(Vector.prototype.div); globalThis.p5 = { _friendlyError: function(msg, func) { @@ -1386,7 +1386,7 @@ suite('p5.Vector', function () { suite('heading', function () { beforeEach(function () { - v = new Vector(); + v = new Vector(0,0,0); }); suite('p5.Vector.prototype.heading() [INSTANCE]', function () { @@ -1830,9 +1830,9 @@ suite('p5.Vector', function () { incoming_y, incoming_z ); - x_target = new Vector(); - y_target = new Vector(); - z_target = new Vector(); + x_target = new Vector(0, 0, 0); + y_target = new Vector(0, 0, 0); + z_target = new Vector(0, 0, 0); x_normal = new Vector(3, 0, 0); y_normal = new Vector(0, 3, 0); @@ -2131,8 +2131,6 @@ suite('p5.Vector', function () { it('should throw error if trying to set w if vector dimensions is less than 4', () => { v = new Vector(1, 2); v.w = 5; - console.log(v); - console.log(v.w); expect(v.w).toBe(0); //TODO: Check this, maybe this should fail }); }); From 27b0d88f44ec66a0e15523982dc64b85d057a5b4 Mon Sep 17 00:00:00 2001 From: Kathrina-dev Date: Fri, 20 Mar 2026 15:45:57 +0000 Subject: [PATCH 087/250] revert back to original structure --- utils/typescript.mjs | 99 +++++++++----------------------------------- 1 file changed, 19 insertions(+), 80 deletions(-) diff --git a/utils/typescript.mjs b/utils/typescript.mjs index 9ee9f2352e..ad119cf80e 100644 --- a/utils/typescript.mjs +++ b/utils/typescript.mjs @@ -26,18 +26,10 @@ const constantsLookup = new Set(); const typedefs = {}; const mutableProperties = new Set(['disableFriendlyErrors']); // Properties that should be mutable, not constants allRawData.forEach(entry => { - if (entry.kind === 'constant') { + if (entry.kind === 'constant' || entry.kind === 'typedef') { constantsLookup.add(entry.name); - } - - // Collect object typedefs so constants referencing them can resolve to proper types - if (entry.kind === 'typedef') { - if ( - entry.properties && - entry.properties.length > 0 && - !entry.properties.every(p => p.name === entry.name) - ) { - typedefs[entry.name] = entry; + if (entry.kind === 'typedef') { + typedefs[entry.name] = entry.type; } } }); @@ -209,16 +201,6 @@ function convertTypeToTypeScript(typeNode, options = {}) { throw new Error(`convertTypeToTypeScript expects an object, got: ${typeof typeNode} - ${JSON.stringify(typeNode)}`); } - if (typeNode.properties && typeNode.properties.length > 0) { - const props = typeNode.properties.map(prop => { - const propType = convertTypeToTypeScript(prop.type, options); - const optional = prop.optional ? '?' : ''; - return `${prop.name}${optional}: ${propType}`; - }); - - return `{ ${props.join('; ')} }`; - } - const { currentClass = null, isInsideNamespace = false, inGlobalMode = false, isConstantDef = false } = options; switch (typeNode.type) { @@ -260,18 +242,19 @@ function convertTypeToTypeScript(typeNode, options = {}) { } } - // If p5 constant: use its typedef when defining it, else reference it as a value via `typeof` + // Check if this is a p5 constant - use typeof since they're defined as values if (constantsLookup.has(typeName)) { - if (isConstantDef && typedefs[typeName]) { - return convertTypeToTypeScript(typedefs[typeName], options); + if (inGlobalMode) { + return `typeof P5.${typeName}`; + } else if (typedefs[typeName]) { + if (isConstantDef) { + return convertTypeToTypeScript(typedefs[typeName], options); + } else { + return `typeof p5.${typeName}` + } + } else { + return `Symbol`; } - return inGlobalMode - ? `typeof P5.${typeName}` - : `typeof ${typeName}`; - } - - if (typedefs[typeName]) { - return typeName; } return typeName; @@ -385,11 +368,6 @@ const typescriptStrategy = { const processed = processData(rawData, typescriptStrategy); -// Augment constantsLookup with processed constants -Object.keys(processed.consts).forEach(name => { - constantsLookup.add(name); -}); - function normalizeIdentifier(name) { return ( '0123456789'.includes(name[0]) || @@ -617,22 +595,6 @@ function generateClassDeclaration(classData) { function generateTypeDefinitions() { let output = '// This file is auto-generated from JSDoc documentation\n\n'; - const strandsMethods = processStrandsFunctions(); - - Object.entries(typedefs).forEach(([name, entry]) => { - if (entry.properties && entry.properties.length > 0) { - const props = entry.properties.map(prop => { - const propType = prop.type ? convertTypeToTypeScript(prop.type) : 'any'; - const optional = prop.optional ? '?' : ''; - return ` ${prop.name}${optional}: ${propType}`; - }); - output += `type ${name} = {\n${props.join(';\n')};\n};\n\n`; - } else { - const tsType = convertTypeToTypeScript(entry.type || entry); - output += `type ${name} = ${tsType};\n\n`; - } - }); - // First, define all constants at the top level with their actual values const seenConstants = new Set(); const p5Constants = processed.classitems.filter(item => { @@ -644,9 +606,6 @@ function generateTypeDefinitions() { if (seenConstants.has(item.name)) { return false; } - if (item.name in typedefs) { - return false; - } seenConstants.add(item.name); return true; } @@ -659,26 +618,12 @@ function generateTypeDefinitions() { output += formatJSDocComment(constant.description, 0) + '\n'; output += ' */\n'; } - let type; - // Avoid invalid self-referential types like `typeof FOO` - if ( - constant.type?.type === 'NameExpression' && - constant.type.name === constant.name - ) { - // Self-referential constant → fallback - type = 'number'; - } else { - type = convertTypeToTypeScript(constant.type, { - isInsideNamespace: false, - isConstantDef: true - }); - } + const type = convertTypeToTypeScript(constant.type, { isInsideNamespace: false, isConstantDef: true }); const isMutable = mutableProperties.has(constant.name); const declaration = isMutable ? 'declare let' : 'declare const'; output += `${declaration} ${constant.name}: ${type};\n\n`; // Duplicate with a private identifier so we can re-export in the namespace later output += `${declaration} __${constant.name}: typeof ${constant.name};\n\n`; - }); // Generate main p5 class @@ -700,6 +645,7 @@ function generateTypeDefinitions() { }); // Add strands functions to p5 instance + const strandsMethods = processStrandsFunctions(); strandsMethods.forEach(method => { output += generateMethodDeclaration(method, p5Options); }); @@ -721,16 +667,9 @@ function generateTypeDefinitions() { output += '\n'; - p5Constants.forEach(constant => { - const isTypedefTyped = - constant.type?.type === 'NameExpression' && - constant.type?.name in typedefs; - if (isTypedefTyped) { - output += `${mutableProperties.has(constant.name) ? 'let' : 'const'} ${constant.name}: ${constant.type.name};\n`; - } else { - output += `${mutableProperties.has(constant.name) ? 'let' : 'const'} ${constant.name}: typeof __${constant.name};\n`; - } + p5Constants.forEach(constant => { + output += `${mutableProperties.has(constant.name) ? 'let' : 'const'} ${constant.name}: typeof __${constant.name};\n`; }); output += '\n'; @@ -881,4 +820,4 @@ console.log('TypeScript definitions generated successfully!'); // Apply patches console.log('Applying TypeScript patches...'); -applyPatches(); +applyPatches(); \ No newline at end of file From f29c5b8f68f05452be4820f5b477076eb267ff29 Mon Sep 17 00:00:00 2001 From: Kathrina-dev Date: Fri, 20 Mar 2026 18:04:45 +0000 Subject: [PATCH 088/250] added new function to handle typedef properties --- utils/typescript.mjs | 141 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 135 insertions(+), 6 deletions(-) diff --git a/utils/typescript.mjs b/utils/typescript.mjs index ad119cf80e..3b01110527 100644 --- a/utils/typescript.mjs +++ b/utils/typescript.mjs @@ -29,7 +29,8 @@ allRawData.forEach(entry => { if (entry.kind === 'constant' || entry.kind === 'typedef') { constantsLookup.add(entry.name); if (entry.kind === 'typedef') { - typedefs[entry.name] = entry.type; + // Store the full entry so we have access to both .type and .properties + typedefs[entry.name] = entry; } } }); @@ -242,15 +243,29 @@ function convertTypeToTypeScript(typeNode, options = {}) { } } - // Check if this is a p5 constant - use typeof since they're defined as values + // Check if this is a p5 constant/typedef if (constantsLookup.has(typeName)) { + const typedefEntry = typedefs[typeName]; + + // Use interface name for object typedefs unless resolving the constant definition itself + if (typedefEntry && hasTypedefProperties(typedefEntry) && !isConstantDef) { + if (inGlobalMode) { + return `P5.${typeName}`; + } else if (isInsideNamespace) { + return typeName; + } else { + return `p5.${typeName}`; + } + } + + // Fallback to typeof or primitive resolution for alias-style typedefs if (inGlobalMode) { return `typeof P5.${typeName}`; - } else if (typedefs[typeName]) { + } else if (typedefEntry) { if (isConstantDef) { - return convertTypeToTypeScript(typedefs[typeName], options); + return convertTypeToTypeScript(typedefEntry.type, options); } else { - return `typeof p5.${typeName}` + return `typeof p5.${typeName}`; } } else { return `Symbol`; @@ -330,6 +345,105 @@ function convertTypeToTypeScript(typeNode, options = {}) { } } +// Check if typedef represents a real object shape +function hasTypedefProperties(typedefEntry) { + if (!Array.isArray(typedefEntry.properties) || typedefEntry.properties.length === 0) { + return false; + } + // Reject self-referential single-property typedefs + if ( + typedefEntry.properties.length === 1 && + typedefEntry.properties[0].name === typedefEntry.name + ) { + return false; + } + return true; +} + +// Convert JSDoc FunctionType into a TypeScript function signature string +function convertFunctionTypeForInterface(typeNode, options) { + const params = (typeNode.params || []) + .map((param, i) => { + let typeObj; + let paramName; + if (param.type === 'ParameterType') { + typeObj = param.expression; + paramName = param.name ?? `p${i}`; + } else if (typeof param.type === 'object' && param.type !== null) { + typeObj = param.type; + paramName = param.name ?? `p${i}`; + } else { + // param itself is a plain type node + typeObj = param; + paramName = `p${i}`; + } + const paramType = convertTypeToTypeScript(typeObj, options); + return `${paramName}: ${paramType}`; + }) + .join(', '); + + const returnType = typeNode.result + ? convertTypeToTypeScript(typeNode.result, options) + : 'void'; + + // Normalise 'undefined' return to 'void' for idiomatic TypeScript + const normalisedReturn = returnType === 'undefined' ? 'void' : returnType; + + return `(${params}) => ${normalisedReturn}`; +} + +// Generate a TypeScript interface from a typedef with @property fields +function generateTypedefInterface(name, typedefEntry, options = {}, indent = 2) { + const pad = ' '.repeat(indent); + const innerPad = ' '.repeat(indent + 2); + let output = ''; + + if (typedefEntry.description) { + const descStr = typeof typedefEntry.description === 'string' + ? typedefEntry.description + : descriptionStringForTypeScript(typedefEntry.description); + if (descStr) { + output += `${pad}/**\n`; + output += formatJSDocComment(descStr, indent) + '\n'; + output += `${pad} */\n`; + } + } + + output += `${pad}interface ${name} {\n`; + + for (const prop of typedefEntry.properties) { + // Each prop: { name, type, description, optional } + const propName = prop.name; + const rawType = prop.type; + const isOptional = prop.optional || rawType?.type === 'OptionalType'; + const optMark = isOptional ? '?' : ''; + + if (prop.description) { + const propDescStr = typeof prop.description === 'string' + ? prop.description.trim() + : descriptionStringForTypeScript(prop.description); + if (propDescStr) { + output += `${innerPad}/** ${propDescStr} */\n`; + } + } + + if (rawType?.type === 'FunctionType') { + // Render FunctionType properties as method signatures instead of arrow properties + const sig = convertFunctionTypeForInterface(rawType, options); + const arrowIdx = sig.lastIndexOf('=>'); + const paramsPart = sig.substring(0, arrowIdx).trim(); + const retPart = sig.substring(arrowIdx + 2).trim(); + output += `${innerPad}${propName}${paramsPart}: ${retPart};\n`; + } else { + const tsType = rawType ? convertTypeToTypeScript(rawType, options) : 'any'; + output += `${innerPad}${propName}${optMark}: ${tsType};\n`; + } + } + + output += `${pad}}\n\n`; + return output; +} + // Strategy for TypeScript output const typescriptStrategy = { shouldSkipEntry: (entry, context) => { @@ -667,13 +781,20 @@ function generateTypeDefinitions() { output += '\n'; - p5Constants.forEach(constant => { output += `${mutableProperties.has(constant.name) ? 'let' : 'const'} ${constant.name}: typeof __${constant.name};\n`; }); output += '\n'; + // Emit interfaces for typedefs that define object shapes + const namespaceOptions = { isInsideNamespace: true }; + for (const [name, typedefEntry] of Object.entries(typedefs)) { + if (hasTypedefProperties(typedefEntry)) { + output += generateTypedefInterface(name, typedefEntry, namespaceOptions, 2); + } + } + // Generate other classes in namespace Object.values(processed.classes).forEach(classData => { if (classData.name !== 'p5') { @@ -750,6 +871,14 @@ p5: P5; globalDefinitions += '\n'; + // Mirror typedef interfaces for global-mode usage + const globalNamespaceOptions = { isInsideNamespace: true, inGlobalMode: true }; + for (const [name, typedefEntry] of Object.entries(typedefs)) { + if (hasTypedefProperties(typedefEntry)) { + globalDefinitions += generateTypedefInterface(name, typedefEntry, globalNamespaceOptions, 2); + } + } + // Add all real classes as both types and constructors Object.values(processed.classes).forEach(classData => { if (classData.name !== 'p5') { From 82122e1cec0369aa4d5689ac613955fe47dcb6ad Mon Sep 17 00:00:00 2001 From: Nixxx19 Date: Sun, 22 Mar 2026 07:20:12 +0530 Subject: [PATCH 089/250] fix: gracefully handle mixed-material OBJ models instead of crashing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, `parseObj()` used `hasColoredVertices === hasColorlessVertices` to detect inconsistent vertex coloring, but this condition is true in two very different cases: 1. Both flags `false` → model has no faces at all (should never crash) 2. Both flags `true` → model has some faces with material colors and some without (the genuine "mixed" case) Real-world OBJ exports from Blender, Sketchfab, and other tools commonly produce files where only some mesh groups have an explicit MTL material assignment. The previous `throw` caused a hard crash for any such model, which is a significant barrier for beginners trying to use pre-made 3D assets in p5.js. This commit changes the check to `hasColoredVertices && hasColorlessVertices` (only the genuinely inconsistent case) and replaces the thrown error with a `console.warn` + `model.vertexColors = []` reset, so the model still loads and renders using the default fill color. The existing test that expected a throw is updated to assert that the model loads successfully with an empty vertexColors array. --- src/webgl/loading.js | 16 +++++++++++++--- test/unit/io/loadModel.js | 17 ++++++++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 23fe61b123..cb0d04878f 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -652,9 +652,19 @@ function loading(p5, fn){ if (model.vertexNormals.length === 0) { model.computeNormals(); } - if (hasColoredVertices === hasColorlessVertices) { - // If both are true or both are false, throw an error because the model is inconsistent - throw new Error('Model coloring is inconsistent. Either all vertices should have colors or none should.'); + if (hasColoredVertices && hasColorlessVertices) { + // Mixed model: some faces have a material diffuse color assigned, others do not. + // This is common in real-world OBJ exports (e.g. Blender, Sketchfab) where only + // some mesh groups have an explicit MTL material. Rather than crashing the sketch, + // we degrade gracefully: strip the partial vertex colors so the model renders + // with the default fill color instead of corrupted per-vertex coloring. + console.warn( + 'p5.js: This OBJ model has mixed material coloring — some faces have a ' + + 'material diffuse color and some do not. Vertex colors will not be applied. ' + + 'Consider assigning a material to every face group in your 3D software, ' + + 'or use a model where all faces share the same material.' + ); + model.vertexColors = []; } return model; diff --git a/test/unit/io/loadModel.js b/test/unit/io/loadModel.js index f88a5807cc..e010390586 100644 --- a/test/unit/io/loadModel.js +++ b/test/unit/io/loadModel.js @@ -79,11 +79,18 @@ suite('loadModel', function() { assert.deepEqual(model.vertexColors, expectedColors); }); - test('inconsistent vertex coloring throws error', async function() { - // Attempt to load the model and catch the error - await expect(mockP5Prototype.loadModel(inconsistentColorObjFile)) - .rejects - .toThrow('Model coloring is inconsistent. Either all vertices should have colors or none should.'); + test('mixed material coloring loads model with empty vertexColors instead of crashing', async function() { + // eg1.obj has some faces without a material and some with one. + // Real-world exports from Blender/Sketchfab frequently produce this structure. + // The loader should degrade gracefully rather than throwing, so beginners' + // sketches don't crash when loading common 3D assets. + const model = await mockP5Prototype.loadModel(inconsistentColorObjFile); + assert.instanceOf(model, Geometry); + assert.equal( + model.vertexColors.length, + 0, + 'Mixed-material model should have no vertex colors (graceful degradation)' + ); }); test('missing MTL file shows OBJ model without vertexColors', async function() { From bc62e8cfb98b33ff4a4cdb69ddbb192ff3cfa435 Mon Sep 17 00:00:00 2001 From: Kathrina-dev Date: Mon, 23 Mar 2026 14:45:57 +0000 Subject: [PATCH 090/250] added condition to allow filterColorHook to pass through p5Constants --- utils/typescript.mjs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/utils/typescript.mjs b/utils/typescript.mjs index 3b01110527..751ff117f8 100644 --- a/utils/typescript.mjs +++ b/utils/typescript.mjs @@ -247,8 +247,8 @@ function convertTypeToTypeScript(typeNode, options = {}) { if (constantsLookup.has(typeName)) { const typedefEntry = typedefs[typeName]; - // Use interface name for object typedefs unless resolving the constant definition itself - if (typedefEntry && hasTypedefProperties(typedefEntry) && !isConstantDef) { + // Use interface name for object-shaped typedefs in all contexts + if (typedefEntry && hasTypedefProperties(typedefEntry)) { if (inGlobalMode) { return `P5.${typeName}`; } else if (isInsideNamespace) { @@ -720,6 +720,10 @@ function generateTypeDefinitions() { if (seenConstants.has(item.name)) { return false; } + // Skip typedefs that have real object shapes + if (typedefs[item.name] && hasTypedefProperties(typedefs[item.name])) { + return false; + } seenConstants.add(item.name); return true; } From 3559fadd4d9279336fbd39285464da1fb5dad145 Mon Sep 17 00:00:00 2001 From: dhowe Date: Mon, 23 Mar 2026 16:13:41 +0000 Subject: [PATCH 091/250] fix to bug with color in tint --- src/image/loading_displaying.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 3f6548032a..1ad7a9c80a 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -1249,7 +1249,7 @@ function loadingDisplaying(p5, fn){ } if (args && args.length) { const c = this.color(...args); - this._renderer.states.setValue('tint', c._getRGBA([255, 255, 255, 255])); + this._renderer.states.setValue('tint', c); } return this; }; From 4c51317f82106221d6b55c8d0b3c7023152b3287 Mon Sep 17 00:00:00 2001 From: dhowe Date: Mon, 23 Mar 2026 17:14:58 +0000 Subject: [PATCH 092/250] merge from dev2.0 --- docs/parameterData.json | 38 +++++++++++++++++++++++--------- test/unit/core/properties.js | 20 +++++------------ test/unit/webgl/p5.RendererGL.js | 33 ++++++++++++++++++--------- 3 files changed, 56 insertions(+), 35 deletions(-) diff --git a/docs/parameterData.json b/docs/parameterData.json index 6cf76f3c4a..41da80e4ab 100644 --- a/docs/parameterData.json +++ b/docs/parameterData.json @@ -305,6 +305,7 @@ }, "ellipseMode": { "overloads": [ + [], [ "CENTER|RADIUS|CORNER|CORNERS" ] @@ -808,6 +809,7 @@ }, "rectMode": { "overloads": [ + [], [ "CENTER|RADIUS|CORNER|CORNERS" ] @@ -878,7 +880,8 @@ "ARROW|CROSS|HAND|MOVE|TEXT|WAIT|String", "Number?", "Number?" - ] + ], + [] ] }, "createElement": { @@ -1086,7 +1089,8 @@ "overloads": [ [ "ROUND|SQUARE|PROJECT" - ] + ], + [] ] }, "lerp": { @@ -1292,7 +1296,8 @@ "overloads": [ [ "MITER|BEVEL|ROUND" - ] + ], + [] ] }, "noCursor": { @@ -1391,7 +1396,8 @@ "overloads": [ [ "Number" - ] + ], + [] ] }, "degrees": { @@ -2268,6 +2274,7 @@ }, "colorMode": { "overloads": [ + [], [ "RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH", "Number?" @@ -2492,6 +2499,7 @@ }, "tint": { "overloads": [ + [], [ "Number", "Number", @@ -2631,6 +2639,7 @@ }, "fill": { "overloads": [ + [], [ "Number", "Number", @@ -2680,7 +2689,8 @@ [ "LEFT|CENTER|RIGHT?", "TOP|BOTTOM|CENTER|BASELINE?" - ] + ], + [] ] }, "textAscent": { @@ -2701,7 +2711,8 @@ "overloads": [ [ "Number?" - ] + ], + [] ] }, "textFont": { @@ -2709,7 +2720,8 @@ [ "p5.Font|String|Object?", "Number?" - ] + ], + [] ] }, "textSize": { @@ -2854,6 +2866,7 @@ }, "imageMode": { "overloads": [ + [], [ "CORNER|CORNERS|CENTER" ] @@ -2983,6 +2996,7 @@ }, "stroke": { "overloads": [ + [], [ "Number", "Number", @@ -3227,6 +3241,7 @@ }, "blendMode": { "overloads": [ + [], [ "BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|REMOVE|SUBTRACT" ] @@ -3298,7 +3313,8 @@ "overloads": [ [ "Object" - ] + ], + [] ] }, "linePerspective": { @@ -3387,7 +3403,8 @@ "overloads": [ [ "IMAGE|NORMAL" - ] + ], + [] ] }, "setCamera": { @@ -3409,7 +3426,8 @@ [ "CLAMP|REPEAT|MIRROR", "CLAMP|REPEAT|MIRROR?" - ] + ], + [] ] }, "normalMaterial": { diff --git a/test/unit/core/properties.js b/test/unit/core/properties.js index 04e44b3978..e7def6900e 100644 --- a/test/unit/core/properties.js +++ b/test/unit/core/properties.js @@ -7,16 +7,6 @@ suite('Set/get properties', function() { sketch.draw = function () { }; }); - /*beforeEach(function () { - myp5 = new p5(function (p) { - p.setup = function () { }; - p.draw = function () { }; - }); - }); - afterEach(function () { - myp5.remove(); - });*/ - let getters = { fill: new p5.Color([100, 200, 50]), stroke: new p5.Color([200, 100, 50, 100]), @@ -24,7 +14,7 @@ suite('Set/get properties', function() { rectMode: p.CENTER, colorMode: p.HSB, - blendMode: 'source-over', + blendMode: p.BLEND, imageMode: p.CORNER, ellipseMode: p.CORNER, angleMode: p.DEGREES, @@ -32,8 +22,8 @@ suite('Set/get properties', function() { strokeWeight: 6, strokeCap: p.ROUND, strokeJoin: p.MITER, + cursor: p.HAND, pixelDensity: 1, - cursor: 'pointer', bezierOrder: 2, splineProperties: { ends: p.EXCLUDE, tightness: -5 }, @@ -50,11 +40,11 @@ suite('Set/get properties', function() { textDirection: 1, textWeight: 1 - // rotate: p.PI, see #8278 + // see #8278 + // rotate: p.PI, // translate: { x: 1, y: 2 }, // scale: { x: 1, y: 2 }, - // background: new p5.Color([100, 100, 50]), - + // background: new p5.Color([100, 100, 50]) }; Object.keys(getters).forEach(prop => { diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index b42562051f..5cb1c7f82d 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1495,28 +1495,41 @@ suite('p5.RendererGL', function() { assert.deepEqual(myp5._renderer.states.tint, [255, 255, 255, 255]); }); + + test('tint value is modified correctly when tint() is called', function() { + + function assertDeepEqualColor(actual, expected) { + if (typeof actual === 'string' && typeof expected === 'string') { + assert.equal(actual, expected); + } else { + assert.equal(actual.toString(), myp5.color(expected).toString()); + } + } myp5.createCanvas(100, 100, myp5.WEBGL); myp5.tint(0, 153, 204, 126); - assert.deepEqual(myp5._renderer.states.tint, [0, 153, 204, 126]); + assertDeepEqualColor(myp5._renderer.states.tint, [0, 153, 204, 126]); myp5.tint(100, 120, 140); - assert.deepEqual(myp5._renderer.states.tint, [100, 120, 140, 255]); + assertDeepEqualColor(myp5._renderer.states.tint, [100, 120, 140, 255]); myp5.tint('violet'); - assert.deepEqual(myp5._renderer.states.tint, [238, 130, 238, 255]); + // Note that in WEBGL mode, we don't convert color strings to arrays until the shader, + // so the tint state is still the string 'violet' at this point, not the array [238, 130, 238, 255]. + //assertDeepEqualColor(myp5._renderer.states.tint, [238, 130, 238, 255]); + assertDeepEqualColor(myp5._renderer.states.tint, 'violet'); myp5.tint(100); - assert.deepEqual(myp5._renderer.states.tint, [100, 100, 100, 255]); + assertDeepEqualColor(myp5._renderer.states.tint, [100, 100, 100, 255]); myp5.tint(100, 126); - assert.deepEqual(myp5._renderer.states.tint, [100, 100, 100, 126]); + assertDeepEqualColor(myp5._renderer.states.tint, [100, 100, 100, 126]); myp5.tint([100, 126, 0, 200]); - assert.deepEqual(myp5._renderer.states.tint, [100, 126, 0, 200]); + assertDeepEqualColor(myp5._renderer.states.tint, [100, 126, 0, 200]); myp5.tint([100, 126, 0]); - assert.deepEqual(myp5._renderer.states.tint, [100, 126, 0, 255]); + assertDeepEqualColor(myp5._renderer.states.tint, [100, 126, 0, 255]); myp5.tint([100]); - assert.deepEqual(myp5._renderer.states.tint, [100, 100, 100, 255]); + assertDeepEqualColor(myp5._renderer.states.tint, [100, 100, 100, 255]); myp5.tint([100, 126]); - assert.deepEqual(myp5._renderer.states.tint, [100, 100, 100, 126]); + assertDeepEqualColor(myp5._renderer.states.tint, [100, 100, 100, 126]); myp5.tint(myp5.color(255, 204, 0)); - assert.deepEqual(myp5._renderer.states.tint, [255, 204, 0, 255]); + assertDeepEqualColor(myp5._renderer.states.tint, [255, 204, 0, 255]); }); test('tint should be reset after draw loop', function() { From 9d9e73c9213aa34cf08e8cedb1963cd40f564729 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 23 Mar 2026 16:41:20 -0400 Subject: [PATCH 093/250] Make sure there are FES messages when WebGPU is not loaded --- src/core/p5.Renderer3D.js | 198 +++++++++++++++++++++++++++ src/core/rendering.js | 8 ++ src/webgpu/p5.RendererWebGPU.js | 236 ++++++++++---------------------- 3 files changed, 275 insertions(+), 167 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 4241a56157..53b4f37fd6 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -1985,8 +1985,206 @@ export class Renderer3D extends Renderer { } } +const webGPUAddonMessage = 'Add the WebGPU add-on to your project and pass WEBGPU as the last argument to createCanvas.'; + function renderer3D(p5, fn) { p5.Renderer3D = Renderer3D; + + /** + * Creates a storage buffer for use in compute shaders. + * + * @method createStorage + * @beta + * @webgpu + * @webgpuOnly + * @param {Number|Array|Float32Array|Object[]} dataOrCount Either a number specifying the count of floats, + * an array/Float32Array of floats, or an array of objects describing struct elements. + * @returns {p5.StorageBuffer} A storage buffer. + */ + fn.createStorage = function (dataOrCount) { + if (!this._renderer.createStorage) { + p5._friendlyError( + `createStorage() is only available with the WebGPU renderer. ${webGPUAddonMessage}`, + 'createStorage' + ); + return; + } + return this._renderer.createStorage(dataOrCount); + }; + + /** + * Returns the base compute shader. + * + * Calling `buildComputeShader(shaderFunction)` is equivalent to + * calling `baseComputeShader().modify(shaderFunction)`. + * + * @method baseComputeShader + * @beta + * @webgpu + * @webgpuOnly + * @returns {p5.Shader} The base compute shader. + */ + fn.baseComputeShader = function () { + if (!this._renderer.baseComputeShader) { + p5._friendlyError( + `baseComputeShader() is only available with the WebGPU renderer. ${webGPUAddonMessage}`, + 'baseComputeShader' + ); + return; + } + return this._renderer.baseComputeShader(); + }; + + /** + * Create a new compute shader using p5.strands. + * + * A compute shader lets you run many calculations all at once on your GPU. They + * are similar to a `compute()` + * and passing the shader in, along with the number of iterations. + * + * ```js example + * let particles; + * let computeShader; + * let displayShader; + * let instance; + * const numParticles = 50; + * + * async function setup() { + * await createCanvas(100, 100, WEBGPU); + * + * let data = []; + * for (let i = 0; i < numParticles; i++) { + * data.push({ + * position: createVector( + * random(-40, 40), + * random(-40, 40) + * ), + * velocity: createVector( + * random(-1, 1), + * random(-1, 1) + * ), + * }); + * } + * particles = createStorage(data); + * + * computeShader = buildComputeShader(simulate); + * displayShader = buildMaterialShader(display); + * instance = buildGeometry(drawParticle); + * } + * + * function drawParticle() { + * sphere(3); + * } + * + * function simulate() { + * let r = 3; + * let data = uniformStorage(particles); + * let idx = iteration.index.x; + * let pos = data[idx].position; + * let vel = data[idx].velocity; + * pos = pos + vel; + * if (pos.x > width/2 - r || pos.x < -height/2 + r) { + * vel.x = -vel.x; + * pos.x = clamp(pos.x, -width/2 + r, width/2 - r); + * } + * if (pos.y > height/2 - r || pos.y < -height/2 + r) { + * vel.y = -vel.y; + * pos.y = clamp(pos.y, -height/2 + r, height/2 - r); + * } + * data[idx].position = pos; + * data[idx].velocity = vel; + * } + * + * function display() { + * let data = uniformStorage(particles); + * worldInputs.begin(); + * let pos = data[instanceID()].position; + * worldInputs.position.xy += pos; + * worldInputs.end(); + * } + * + * function draw() { + * background(30); + * compute(computeShader, numParticles); + * noStroke(); + * fill(255); + * lights(); + * shader(displayShader); + * model(instance, numParticles); + * } + * ``` + * + * @method buildComputeShader + * @beta + * @webgpu + * @webgpuOnly + * @param {Function} callback A function building a p5.strands compute shader. + * @returns {p5.Shader} The compute shader. + */ + fn.buildComputeShader = function (cb, context) { + if (!this._renderer.baseComputeShader) { + p5._friendlyError( + `buildComputeShader() is only available with the WebGPU renderer. ${webGPUAddonMessage}`, + 'buildComputeShader' + ); + return; + } + return this.baseComputeShader().modify(cb, context, { hook: 'iteration' }); + }; + + /** + * Dispatches a compute shader to run on the GPU. + * + * @method compute + * @beta + * @webgpu + * @webgpuOnly + * @param {p5.Shader} shader The compute shader to run. + * @param {Number} x Number of invocations in the X dimension. + * @param {Number} [y=1] Number of invocations in the Y dimension. + * @param {Number} [z=1] Number of invocations in the Z dimension. + */ + fn.compute = function (shader, x, y, z) { + if (!this._renderer.compute) { + p5._friendlyError( + `compute() is only available with the WebGPU renderer. ${webGPUAddonMessage}`, + 'compute' + ); + return; + } + this._renderer.compute(shader, x, y, z); + }; + + /** + * Information about the current iteration of a compute shader. + * + * Use it inside a + * `buildComputeShader()` + * function to write a loop that runs in parallel on the GPU. + * + * `iteration` has the following properties: + * - `index`: a three-component vector with the current index + * across all dimensions passed to + * `compute()`. For example, use + * `iteration.index.x` to get the index when looping in one dimension. + * - `localIndex`: an integer index of the thread within its workgroup. + * - `localId`: a three-component integer vector with the thread's position + * within its workgroup. + * - `workgroupId`: a three-component integer vector identifying which + * workgroup this thread belongs to. + * + * @property {Object} iteration + * @beta + * @webgpu + * @webgpuOnly + */ } export default renderer3D; diff --git a/src/core/rendering.js b/src/core/rendering.js index dd9051508d..6a3346f316 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -125,6 +125,14 @@ function rendering(p5, fn){ args.unshift(renderer); } + if (!renderers[selectedRenderer]) { + if (selectedRenderer === constants.WEBGPU) { + p5._friendlyError(`To create a WEBGPU canvas, remember to add the WebGPU add-on to your project.`); + } else { + p5._friendlyError(`We weren't able to find a renderer called ${selectedRenderer}.`); + } + } + // Init our graphics renderer if(this._renderer) this._renderer.remove(); this._renderer = new renderers[selectedRenderer](this, w, h, true, ...args); diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 8477465ba2..a3d3373e7b 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -48,6 +48,75 @@ function rendererWebGPU(p5, fn) { this._renderer = renderer; this._schema = schema; } + + /** + * Updates the contents of the storage buffer with new data. + * + * The format of the new data must match the format used when the buffer + * was created with `createStorage()`: + * an array of objects for struct buffers, or an array/Float32Array of + * floats for plain float buffers. + * + * @method update + * @for p5.StorageBuffer + * @beta + * @webgpu + * @webgpuOnly + * @param {Array|Float32Array} data The new data to write into the buffer. + */ + update(data) { + const device = this._renderer.device; + + if (this._schema !== null) { + // Buffer was created with a struct array + if ( + !Array.isArray(data) || + data.length === 0 || + typeof data[0] !== 'object' || + Array.isArray(data[0]) + ) { + throw new Error( + 'update() expects an array of objects matching the original struct format' + ); + } + + const newSchema = this._renderer._inferStructSchema(data[0]); + if (newSchema.structBody !== this._schema.structBody) { + throw new Error( + `update() data structure doesn't match the original.\n` + + ` Expected: ${this._schema.structBody}\n` + + ` Got: ${newSchema.structBody}` + ); + } + + const packed = this._renderer._packStructArray(data, this._schema); + if (packed.byteLength > this.size) { + throw new Error( + `update() data (${packed.byteLength} bytes) exceeds buffer size (${this.size} bytes)` + ); + } + device.queue.writeBuffer(this.buffer, 0, packed); + } else { + // Buffer was created with a float array + let floatData; + if (data instanceof Float32Array) { + floatData = data; + } else if (Array.isArray(data)) { + floatData = new Float32Array(data); + } else { + throw new Error( + 'update() expects a Float32Array or array of numbers for this buffer' + ); + } + + if (floatData.byteLength > this.size) { + throw new Error( + `update() data (${floatData.byteLength} bytes) exceeds buffer size (${this.size} bytes)` + ); + } + device.queue.writeBuffer(this.buffer, 0, floatData); + } + } } /** @@ -3653,173 +3722,6 @@ ${hookUniformFields}} return this._renderer._setAttributes(key, value); } - /** - * Creates a storage buffer for use in compute shaders. - * - * @method createStorage - * @beta - * @webgpu - * @webgpuOnly - * @param {Number|Array|Float32Array|Object[]} dataOrCount Either a number specifying the count of floats, - * an array/Float32Array of floats, or an array of objects describing struct elements. - * @returns {p5.StorageBuffer} A storage buffer. - */ - fn.createStorage = function (dataOrCount) { - return this._renderer.createStorage(dataOrCount); - } - - /** - * Returns the base compute shader. - * - * Calling `buildComputeShader(shaderFunction)` is equivalent to - * calling `baseComputeShader().modify(shaderFunction)`. - * - * @method baseComputeShader - * @beta - * @webgpu - * @webgpuOnly - * @returns {p5.Shader} The base compute shader. - */ - fn.baseComputeShader = function () { - return this._renderer.baseComputeShader(); - }; - - /** - * Create a new compute shader using p5.strands. - * - * A compute shader lets you run many calculations all at once on your GPU. They - * are similar to a `compute()` - * and passing the shader in, along with the number of iterations. - * - * ```js example - * let particles; - * let computeShader; - * let displayShader; - * let instance; - * const numParticles = 50; - * - * async function setup() { - * await createCanvas(100, 100, WEBGPU); - * - * let data = []; - * for (let i = 0; i < numParticles; i++) { - * data.push({ - * position: createVector( - * random(-40, 40), - * random(-40, 40) - * ), - * velocity: createVector( - * random(-1, 1), - * random(-1, 1) - * ), - * }); - * } - * particles = createStorage(data); - * - * computeShader = buildComputeShader(simulate); - * displayShader = buildMaterialShader(display); - * instance = buildGeometry(drawParticle); - * } - * - * function drawParticle() { - * sphere(3); - * } - * - * function simulate() { - * let r = 3; - * let data = uniformStorage(particles); - * let idx = iteration.index.x; - * let pos = data[idx].position; - * let vel = data[idx].velocity; - * pos = pos + vel; - * if (pos.x > width/2 - r || pos.x < -height/2 + r) { - * vel.x = -vel.x; - * pos.x = clamp(pos.x, -width/2 + r, width/2 - r); - * } - * if (pos.y > height/2 - r || pos.y < -height/2 + r) { - * vel.y = -vel.y; - * pos.y = clamp(pos.y, -height/2 + r, height/2 - r); - * } - * data[idx].position = pos; - * data[idx].velocity = vel; - * } - * - * function display() { - * let data = uniformStorage(particles); - * worldInputs.begin(); - * let pos = data[instanceID()].position; - * worldInputs.position.xy += pos; - * worldInputs.end(); - * } - * - * function draw() { - * background(30); - * compute(computeShader, numParticles); - * noStroke(); - * fill(255); - * lights(); - * shader(displayShader); - * model(instance, numParticles); - * } - * ``` - * - * @method buildComputeShader - * @beta - * @webgpu - * @webgpuOnly - * @param {Function} callback A function building a p5.strands compute shader. - * @returns {p5.Shader} The compute shader. - */ - fn.buildComputeShader = function (cb, context) { - return this.baseComputeShader().modify(cb, context, { hook: 'iteration' }); - }; - - /** - * Dispatches a compute shader to run on the GPU. - * - * @method compute - * @beta - * @webgpu - * @webgpuOnly - * @param {p5.Shader} shader The compute shader to run. - * @param {Number} x Number of invocations in the X dimension. - * @param {Number} [y=1] Number of invocations in the Y dimension. - * @param {Number} [z=1] Number of invocations in the Z dimension. - */ - fn.compute = function (shader, x, y, z) { - this._renderer.compute(shader, x, y, z); - }; - - /** - * Information about the current iteration of a compute shader. - - * Use it inside a - * `buildComputeShader()` - * function to write a loop that runs in parallel on the GPU. - * - * `iteration` has the following properties: - * - `index`: a three-component vector with the current index - * across all dimensions passed to - * `compute()`. For example, use - * `iteration.index.x` to get the index when looping in one dimension. - * - `localIndex`: an integer index of the thread within its workgroup. - * - `localId`: a three-component integer vector with the thread's position - * within its workgroup. - * - `workgroupId`: a three-component integer vector identifying which - * workgroup this thread belongs to. - * - * @property {Object} iteration - * @beta - * @webgpu - * @webgpuOnly - */ } export default rendererWebGPU; From 9ed376ddc49c5f43ab227c596f02bc9ab1b25ee8 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 23 Mar 2026 17:51:00 -0400 Subject: [PATCH 094/250] Just expose the index for now --- preview/index.html | 2 +- src/core/p5.Renderer3D.js | 77 +++++++++++++++++++++++++++----- src/strands/p5.strands.js | 12 ++++- src/strands/strands_api.js | 3 ++ src/webgpu/p5.RendererWebGPU.js | 2 +- src/webgpu/shaders/compute.js | 22 +++------ test/unit/visual/cases/webgpu.js | 10 ++--- 7 files changed, 91 insertions(+), 37 deletions(-) diff --git a/preview/index.html b/preview/index.html index 5afac77ed2..055f642f2e 100644 --- a/preview/index.html +++ b/preview/index.html @@ -111,7 +111,7 @@ const bounds = p.uniformVec2(() => [p.width / 2 - RADIUS, p.height / 2 - RADIUS]); const deltaTime = p.uniformFloat(() => p.deltaTime * 0.1); - const idx = iteration.index.x; + const idx = p.index.x; // Read current position and velocity let position = particles[idx].position; diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 53b4f37fd6..7cbfebd476 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -2054,6 +2054,65 @@ function renderer3D(p5, fn) { * let computeShader; * let displayShader; * let instance; + * const numParticles = 100; + * + * async function setup() { + * await createCanvas(100, 100, WEBGPU); + * particles = createStorage(makeParticles(width / 2, height / 2)); + * computeShader = buildComputeShader(simulate); + * displayShader = buildMaterialShader(display); + * instance = buildGeometry(drawParticle); + * } + * + * function makeParticles(x, y) { + * let data = []; + * for (let i = 0; i < numParticles; i++) { + * let angle = (i / numParticles) * TWO_PI; + * let speed = random(0.5, 2); + * data.push({ + * position: createVector(x, y), + * velocity: createVector(cos(angle) * speed, sin(angle) * speed), + * }); + * } + * return data; + * } + * + * function drawParticle() { + * sphere(2); + * } + * + * function simulate() { + * let data = uniformStorage(particles); + * let idx = index.x; + * data[idx].position = data[idx].position + data[idx].velocity; + * } + * + * function display() { + * let data = uniformStorage(particles); + * worldInputs.begin(); + * let pos = data[instanceID()].position; + * worldInputs.position.xy += pos - createVector(width / 2, height / 2); + * worldInputs.end(); + * } + * + * function draw() { + * background(30); + * if (frameCount % 60 === 0) { + * particles.update(makeParticles(random(width), random(height))); + * } + * compute(computeShader, numParticles); + * noStroke(); + * fill(255, 200, 50); + * shader(displayShader); + * model(instance, numParticles); + * } + * ``` + * + * ```js example + * let particles; + * let computeShader; + * let displayShader; + * let instance; * const numParticles = 50; * * async function setup() { @@ -2086,7 +2145,7 @@ function renderer3D(p5, fn) { * function simulate() { * let r = 3; * let data = uniformStorage(particles); - * let idx = iteration.index.x; + * let idx = index.x; * let pos = data[idx].position; * let vel = data[idx].velocity; * pos = pos + vel; @@ -2169,18 +2228,12 @@ function renderer3D(p5, fn) { * `buildComputeShader()` * function to write a loop that runs in parallel on the GPU. * - * `iteration` has the following properties: - * - `index`: a three-component vector with the current index - * across all dimensions passed to - * `compute()`. For example, use - * `iteration.index.x` to get the index when looping in one dimension. - * - `localIndex`: an integer index of the thread within its workgroup. - * - `localId`: a three-component integer vector with the thread's position - * within its workgroup. - * - `workgroupId`: a three-component integer vector identifying which - * workgroup this thread belongs to. + * `index` is a three-component vector with the current index + * across all dimensions passed to + * `compute()`. For example, use + * `index.x` to get the index when looping in one dimension. * - * @property {Object} iteration + * @property index * @beta * @webgpu * @webgpuOnly diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index e420586201..1b1bd2ac8f 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -116,6 +116,7 @@ function strands(p5, fn) { const oldModify = p5.Shader.prototype.modify; p5.Shader.prototype.modify = function (shaderModifier, scope = {}, options = {}) { + const fnOverrides = {}; try { if ( shaderModifier instanceof Function || @@ -158,7 +159,13 @@ function strands(p5, fn) { BlockType.GLOBAL, ); pushBlock(strandsContext.cfg, globalScope); - if (options.hook) strandsContext.renderer._pInst[options.hook].begin(); + if (options.hook) { + strandsContext.renderer._pInst[options.hook].begin(); + for (const key of strandsContext.renderer._pInst[options.hook]._properties) { + fnOverrides[key] = fn[key]; + fn[key] = strandsContext.renderer._pInst[options.hook][key]; + } + } if (strandsContext.renderer?._pInst?._runStrandsInGlobalMode) { withTempGlobalMode(strandsContext.renderer._pInst, strandsCallback); } else { @@ -177,6 +184,9 @@ function strands(p5, fn) { return oldModify.call(this, shaderModifier); } } finally { + for (const key in fnOverrides) { + fn[key] = fnOverrides[key]; + } // Reset the strands runtime context deinitStrandsContext(strandsContext); } diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 41fe82d243..1cf150ac0b 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -678,9 +678,11 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { if (numStructArgs === 1) { argIdx = hookType.parameters.findIndex(param => param.type.properties); } + hook._properties = []; for (let i = 0; i < args.length; i++) { if (i === argIdx) { for (const key of args[argIdx].structProperties || []) { + hook._properties.push(key); Object.defineProperty(hook, key, { get() { return args[argIdx][key]; @@ -695,6 +697,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { hook.set(args[argIdx]); } } else { + hook._properties.push(hookType.parameters[i].name); hook[hookType.parameters[i].name] = args[i]; } } diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index a3d3373e7b..078c46c183 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -3564,7 +3564,7 @@ ${hookUniformFields}} baseComputeShader, { compute: { - 'void iteration': '(inputs: ComputeInputs) {}', + 'void iteration': '(index: vec3) {}', }, } ); diff --git a/src/webgpu/shaders/compute.js b/src/webgpu/shaders/compute.js index 0c2962b43d..b6b323a8cd 100644 --- a/src/webgpu/shaders/compute.js +++ b/src/webgpu/shaders/compute.js @@ -1,11 +1,4 @@ export const baseComputeShader = ` -struct ComputeInputs { - index: vec3, - localIndex: i32, - localId: vec3, - workgroupId: vec3, -} - struct ComputeUniforms { uTotalCount: vec3, } @@ -18,21 +11,16 @@ fn main( @builtin(workgroup_id) workgroupId: vec3, @builtin(local_invocation_index) localIndex: u32 ) { - var inputs: ComputeInputs; - inputs.index = vec3(globalId); + var index = vec3(globalId); if ( - inputs.index.x > uniforms.uTotalCount.x || - inputs.index.y > uniforms.uTotalCount.y || - inputs.index.z > uniforms.uTotalCount.z + index.x > uniforms.uTotalCount.x || + index.y > uniforms.uTotalCount.y || + index.z > uniforms.uTotalCount.z ) { return; } - inputs.localId = vec3(localId); - inputs.workgroupId = vec3(workgroupId); - inputs.localIndex = i32(localIndex); - - HOOK_iteration(inputs); + HOOK_iteration(index); } `; diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index bd79d18e62..2b3939bf03 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -1213,7 +1213,7 @@ visualSuite("WebGPU", function () { const computeShader = p5.buildComputeShader(() => { const buf = p5.uniformStorage('buf', particles); - buf[iteration.index.x].position = [15, -10]; + buf[p5.index.x].position = [15, -10]; }, { p5, particles }); p5.compute(computeShader, 1); @@ -1249,7 +1249,7 @@ visualSuite("WebGPU", function () { const computeShader = p5.buildComputeShader(() => { const buf = p5.uniformStorage('buf', particles); - const idx = iteration.index.x; + const idx = p5.index.x; buf[idx].position = buf[idx].position + buf[idx].velocity; }, { p5, particles }); p5.compute(computeShader, 1); @@ -1287,7 +1287,7 @@ visualSuite("WebGPU", function () { // Store the struct element proxy in a variable and assign through it const computeShader = p5.buildComputeShader(() => { const buf = p5.uniformStorage('buf', particles); - const idx = iteration.index.x; + const idx = p5.index.x; const entry = buf[idx]; entry.position = entry.position + entry.velocity; }, { p5, particles }); @@ -1326,7 +1326,7 @@ visualSuite("WebGPU", function () { // Store a field value in an intermediate variable, update it, write it back const computeShader = p5.buildComputeShader(() => { const buf = p5.uniformStorage('buf', particles); - const idx = iteration.index.x; + const idx = p5.index.x; let pos = buf[idx].position; pos = pos + buf[idx].velocity; buf[idx].position = pos; @@ -1365,7 +1365,7 @@ visualSuite("WebGPU", function () { const computeShader = p5.buildComputeShader(() => { const buf = p5.uniformStorage('buf', particles); - const idx = iteration.index.x; + const idx = p5.index.x; let pos = buf[idx].position; let vel = buf[idx].velocity; pos = pos + vel; From d22cd36fa7e381f0800f6db067aa8a97fce539ce Mon Sep 17 00:00:00 2001 From: dhowe Date: Mon, 23 Mar 2026 23:02:13 +0000 Subject: [PATCH 095/250] using 255 range for tint in shaders --- src/core/p5.Renderer3D.js | 8 ++++---- src/image/loading_displaying.js | 6 ++---- test/unit/webgl/p5.RendererGL.js | 26 ++++++++++++++------------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 4241a56157..129d40df84 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -132,7 +132,7 @@ export class Renderer3D extends Renderer { this.states._useShininess = 1; this.states._useMetalness = 0; - this.states.tint = [255, 255, 255, 255]; + this.states.tint = new Color([255, 255, 255, 255]); this.states.constantAttenuation = 1; this.states.linearAttenuation = 0; @@ -721,10 +721,10 @@ export class Renderer3D extends Renderer { this.states.setValue("enableLighting", false); //reset tint value for new frame - this.states.setValue("tint", [255, 255, 255, 255]); + this.states.setValue("tint", new Color([255, 255, 255, 255])); //Clear depth every frame - this._resetBuffersBeforeDraw() + this._resetBuffersBeforeDraw(); } background(...args) { @@ -1486,7 +1486,7 @@ export class Renderer3D extends Renderer { // works differently and is global p5 state. If the p5 state has // been cleared, we also need to clear the value in uSampler to match. fillShader.setUniform("uSampler", this.states._tex || empty); - fillShader.setUniform("uTint", this.states.tint); + fillShader.setUniform("uTint", this.states.tint.array().map(v => v * 255)); fillShader.setUniform("uHasSetAmbient", this.states._hasSetAmbient); fillShader.setUniform("uAmbientMatColor", this.states.curAmbientColor); diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 1ad7a9c80a..60c5e5d045 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -1243,13 +1243,11 @@ function loadingDisplaying(p5, fn){ * @param {p5.Color} color the tint color */ fn.tint = function(...args) { - // p5._validateParameters('tint', args); if (args.length === 0) { - return this.color(this._renderer.states.tint); // getter + return this._renderer.states.tint; // getter } if (args && args.length) { - const c = this.color(...args); - this._renderer.states.setValue('tint', c); + this._renderer.states.setValue('tint', this.color(...args)); } return this; }; diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 5cb1c7f82d..a2e29f76ce 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1492,7 +1492,7 @@ suite('p5.RendererGL', function() { suite('tint() in WEBGL mode', function() { test('default tint value is set and not null', function() { myp5.createCanvas(100, 100, myp5.WEBGL); - assert.deepEqual(myp5._renderer.states.tint, [255, 255, 255, 255]); + assert.deepEqual(myp5._renderer.states.tint.array(), [255, 255, 255, 255]); }); @@ -1508,28 +1508,30 @@ suite('p5.RendererGL', function() { } myp5.createCanvas(100, 100, myp5.WEBGL); myp5.tint(0, 153, 204, 126); - assertDeepEqualColor(myp5._renderer.states.tint, [0, 153, 204, 126]); + assertDeepEqualColor(myp5._renderer.states.tint.array(), [0, 153, 204, 126]); myp5.tint(100, 120, 140); - assertDeepEqualColor(myp5._renderer.states.tint, [100, 120, 140, 255]); + assertDeepEqualColor(myp5._renderer.states.tint.array(), [100, 120, 140, 255]); + myp5.tint('violet'); // Note that in WEBGL mode, we don't convert color strings to arrays until the shader, // so the tint state is still the string 'violet' at this point, not the array [238, 130, 238, 255]. - //assertDeepEqualColor(myp5._renderer.states.tint, [238, 130, 238, 255]); - assertDeepEqualColor(myp5._renderer.states.tint, 'violet'); + //assertDeepEqualColor(myp5._renderer.states.tint.array(), [238, 130, 238, 255]); + assertDeepEqualColor(myp5._renderer.states.tint.array(), 'violet'); + myp5.tint(100); - assertDeepEqualColor(myp5._renderer.states.tint, [100, 100, 100, 255]); + assertDeepEqualColor(myp5._renderer.states.tint.array(), [100, 100, 100, 255]); myp5.tint(100, 126); - assertDeepEqualColor(myp5._renderer.states.tint, [100, 100, 100, 126]); + assertDeepEqualColor(myp5._renderer.states.tint.array(), [100, 100, 100, 126]); myp5.tint([100, 126, 0, 200]); - assertDeepEqualColor(myp5._renderer.states.tint, [100, 126, 0, 200]); + assertDeepEqualColor(myp5._renderer.states.tint.array(), [100, 126, 0, 200]); myp5.tint([100, 126, 0]); - assertDeepEqualColor(myp5._renderer.states.tint, [100, 126, 0, 255]); + assertDeepEqualColor(myp5._renderer.states.tint.array(), [100, 126, 0, 255]); myp5.tint([100]); - assertDeepEqualColor(myp5._renderer.states.tint, [100, 100, 100, 255]); + assertDeepEqualColor(myp5._renderer.states.tint.array(), [100, 100, 100, 255]); myp5.tint([100, 126]); - assertDeepEqualColor(myp5._renderer.states.tint, [100, 100, 100, 126]); + assertDeepEqualColor(myp5._renderer.states.tint.array(), [100, 100, 100, 126]); myp5.tint(myp5.color(255, 204, 0)); - assertDeepEqualColor(myp5._renderer.states.tint, [255, 204, 0, 255]); + assertDeepEqualColor(myp5._renderer.states.tint.array(), [255, 204, 0, 255]); }); test('tint should be reset after draw loop', function() { From f9460f4a560266ee93a38d79595e78eba58e4f2f Mon Sep 17 00:00:00 2001 From: dhowe Date: Mon, 23 Mar 2026 23:26:01 +0000 Subject: [PATCH 096/250] fix tint-related gl tests --- src/core/p5.Renderer3D.js | 4 ++-- test/unit/webgl/p5.RendererGL.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 129d40df84..5f29e7a60c 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -132,7 +132,7 @@ export class Renderer3D extends Renderer { this.states._useShininess = 1; this.states._useMetalness = 0; - this.states.tint = new Color([255, 255, 255, 255]); + this.states.tint = new Color([1, 1, 1, 1]); this.states.constantAttenuation = 1; this.states.linearAttenuation = 0; @@ -721,7 +721,7 @@ export class Renderer3D extends Renderer { this.states.setValue("enableLighting", false); //reset tint value for new frame - this.states.setValue("tint", new Color([255, 255, 255, 255])); + this.states.setValue("tint", new Color([1,1,1,1])); //Clear depth every frame this._resetBuffersBeforeDraw(); diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index a2e29f76ce..10976c6b51 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1492,7 +1492,7 @@ suite('p5.RendererGL', function() { suite('tint() in WEBGL mode', function() { test('default tint value is set and not null', function() { myp5.createCanvas(100, 100, myp5.WEBGL); - assert.deepEqual(myp5._renderer.states.tint.array(), [255, 255, 255, 255]); + assert.deepEqual(myp5._renderer.states.tint.array(), [1,1,1,1]); }); @@ -1503,7 +1503,7 @@ suite('p5.RendererGL', function() { if (typeof actual === 'string' && typeof expected === 'string') { assert.equal(actual, expected); } else { - assert.equal(actual.toString(), myp5.color(expected).toString()); + assert.deepEqual(actual, myp5.color(expected).array()); } } myp5.createCanvas(100, 100, myp5.WEBGL); @@ -1548,7 +1548,7 @@ suite('p5.RendererGL', function() { }; }); }).then(function(_tint) { - assert.deepEqual(_tint, [255, 255, 255, 255]); + assert.deepEqual(_tint.array(), [1, 1, 1, 1]); }); }); }); From 6a30e34c76cf551a60008b7a8c47577030f833f8 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 23 Mar 2026 19:47:49 -0400 Subject: [PATCH 097/250] More docs --- src/core/p5.Renderer3D.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 7cbfebd476..a460399160 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -2047,7 +2047,16 @@ function renderer3D(p5, fn) { * The function represents one iteration of a loop. * * The compute shader can be run by calling `compute()` - * and passing the shader in, along with the number of iterations. + * and passing the shader in, along with the number of iterations in up to three + * dimensions. Use the `index` vector inside of your + * iteration function to refer to the current iteration of the loop. The `x`, `y`, + * and `z` properties will count up from zero to the count in each dimension passed + * into `compute`. + * + * A compute shader will read from and write to storage, which is often an array of + * numbers or objects. Use `createStorage` to construct + * initial data. Connect your iteration function to the storage by passing the storage + * into `uniformStorage`. * * ```js example * let particles; @@ -2201,6 +2210,10 @@ function renderer3D(p5, fn) { /** * Dispatches a compute shader to run on the GPU. * + * The first parameter, `shader`, is a compute shader created with + * `buildComputeShader`. + * + * * @method compute * @beta * @webgpu From 7e1c8f536bb0463eff482213fd2e1dd5a3a528fe Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 23 Mar 2026 19:54:14 -0400 Subject: [PATCH 098/250] Also add hook props to window and the graphics prototype --- src/core/p5.Renderer3D.js | 2 ++ src/strands/p5.strands.js | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index a460399160..67dd6d5b4a 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -2190,6 +2190,8 @@ function renderer3D(p5, fn) { * ``` * * @method buildComputeShader + * @module 3D + * @submodule p5.strands * @beta * @webgpu * @webgpuOnly diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 1b1bd2ac8f..23000e4988 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -117,6 +117,8 @@ function strands(p5, fn) { p5.Shader.prototype.modify = function (shaderModifier, scope = {}, options = {}) { const fnOverrides = {}; + const windowOverrides = {}; + const graphicsOverrides = {}; try { if ( shaderModifier instanceof Function || @@ -162,8 +164,13 @@ function strands(p5, fn) { if (options.hook) { strandsContext.renderer._pInst[options.hook].begin(); for (const key of strandsContext.renderer._pInst[options.hook]._properties) { + const hookProp = strandsContext.renderer._pInst[options.hook][key]; fnOverrides[key] = fn[key]; - fn[key] = strandsContext.renderer._pInst[options.hook][key]; + fn[key] = hookProp; + windowOverrides[key] = window[key]; + window[key] = hookProp; + graphicsOverrides[key] = p5.Graphics.prototype[key]; + p5.Graphics.prototype[key] = hookProp; } } if (strandsContext.renderer?._pInst?._runStrandsInGlobalMode) { @@ -187,6 +194,12 @@ function strands(p5, fn) { for (const key in fnOverrides) { fn[key] = fnOverrides[key]; } + for (const key in windowOverrides) { + window[key] = windowOverrides[key]; + } + for (const key in graphicsOverrides) { + p5.Graphics[key] = graphicsOverrides[key]; + } // Reset the strands runtime context deinitStrandsContext(strandsContext); } From dfa9d65bf618a88a912575963d0a8280881e18b8 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 23 Mar 2026 20:01:44 -0400 Subject: [PATCH 099/250] Update modules --- src/core/p5.Renderer3D.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 67dd6d5b4a..b74b1c0131 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -1,3 +1,8 @@ +/** + * @module 3D + * @for p5 + */ + import * as constants from "../core/constants"; import { Graphics } from "../core/p5.Graphics"; import { Renderer } from './p5.Renderer'; @@ -1994,6 +1999,7 @@ function renderer3D(p5, fn) { * Creates a storage buffer for use in compute shaders. * * @method createStorage + * @submodule p5.strands * @beta * @webgpu * @webgpuOnly @@ -2019,6 +2025,7 @@ function renderer3D(p5, fn) { * calling `baseComputeShader().modify(shaderFunction)`. * * @method baseComputeShader + * @submodule p5.strands * @beta * @webgpu * @webgpuOnly @@ -2190,7 +2197,6 @@ function renderer3D(p5, fn) { * ``` * * @method buildComputeShader - * @module 3D * @submodule p5.strands * @beta * @webgpu @@ -2217,6 +2223,7 @@ function renderer3D(p5, fn) { * * * @method compute + * @submodule p5.strands * @beta * @webgpu * @webgpuOnly From 3e0c2681e121940e67a32348b0a60bfda698f515 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 23 Mar 2026 20:08:14 -0400 Subject: [PATCH 100/250] Fix shader example, document a bit more --- src/core/p5.Renderer3D.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index b74b1c0131..b3edbee96e 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -2065,6 +2065,10 @@ function renderer3D(p5, fn) { * initial data. Connect your iteration function to the storage by passing the storage * into `uniformStorage`. * + * Often, compute shaders are paired with `model(myGeometry, count)` + * to draw one instance per object in the storage, and a shader that uses + * `instanceID()` to position each instance. + * * ```js example * let particles; * let computeShader; @@ -2107,7 +2111,7 @@ function renderer3D(p5, fn) { * let data = uniformStorage(particles); * worldInputs.begin(); * let pos = data[instanceID()].position; - * worldInputs.position.xy += pos - createVector(width / 2, height / 2); + * worldInputs.position.xy += pos - [width / 2, height / 2]; * worldInputs.end(); * } * From 19c68510d2f2d34c292bb0abf2b82f4f14709862 Mon Sep 17 00:00:00 2001 From: dhowe Date: Tue, 24 Mar 2026 16:28:26 +0000 Subject: [PATCH 101/250] first pass at updating jsdocs for setters --- src/color/setting.js | 12 ++++++++++++ src/image/loading_displaying.js | 10 ++++++++++ src/shape/attributes.js | 6 ++++++ 3 files changed, 28 insertions(+) diff --git a/src/color/setting.js b/src/color/setting.js index 35181f9b19..84fab6182c 100644 --- a/src/color/setting.js +++ b/src/color/setting.js @@ -1095,6 +1095,8 @@ function setting(p5, fn){ * or `HSLA` colors, depending on the current colorMode(). The last parameter * sets the alpha (transparency) value. * + * Calling `fill()` without an argument returns the current fill as a p5.Color object. + * * @method fill * @param {Number} v1 red value if color mode is RGB or hue value if color mode is HSB. * @param {Number} v2 green value if color mode is RGB or saturation value if color mode is HSB. @@ -1283,6 +1285,10 @@ function setting(p5, fn){ * @param {p5.Color} color the fill color. * @chainable */ + /** + * @method fill + * @return {p5.Color} the current fill color. + */ fn.fill = function(...args) { return this._renderer.fill(...args); }; @@ -1413,6 +1419,8 @@ function setting(p5, fn){ * or HSLA colors, depending on the current `colorMode()`. The last parameter * sets the alpha (transparency) value. * + * Calling `stroke()` without an argument returns the current stroke as a p5.Color object. + * * @method stroke * @param {Number} v1 red value if color mode is RGB or hue value if color mode is HSB. * @param {Number} v2 green value if color mode is RGB or saturation value if color mode is HSB. @@ -1599,6 +1607,10 @@ function setting(p5, fn){ * @param {p5.Color} color the stroke color. * @chainable */ + /** + * @method stroke + * @return {p5.Color} the current stroke color. + */ fn.stroke = function(...args) { return this._renderer.stroke(...args); }; diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 60c5e5d045..1e068964b7 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -1138,6 +1138,12 @@ function loadingDisplaying(p5, fn){ * sets the alpha value. For example, `tint(255, 0, 0, 100)` will give images * a red tint and make them transparent. * + * Calling `tint()` without an argument returns the current tint as a p5.Color object. + * + * @method frameRate + * @param {Number} fps number of frames to draw per second. + * @chainable + * * @method tint * @param {Number} v1 red or hue value. * @param {Number} v2 green or saturation value. @@ -1242,6 +1248,10 @@ function loadingDisplaying(p5, fn){ * @method tint * @param {p5.Color} color the tint color */ + /** + * @method tint + * @return {p5.Color} the current tint color + */ fn.tint = function(...args) { if (args.length === 0) { return this._renderer.states.tint; // getter diff --git a/src/shape/attributes.js b/src/shape/attributes.js index e8a942d08f..77ac58ce0c 100644 --- a/src/shape/attributes.js +++ b/src/shape/attributes.js @@ -189,6 +189,8 @@ function attributes(p5, fn){ * constants `CENTER`, `RADIUS`, `CORNER`, and `CORNERS` are defined this way. * JavaScript is a case-sensitive language. * + * Calling `rectMode()` without an argument returns the current rectMode, either `CORNER`, `CORNERS`, `CENTER`, or `RADIUS`. + * * @method rectMode * @param {(CENTER|RADIUS|CORNER|CORNERS)} mode either CORNER, CORNERS, CENTER, or RADIUS * @chainable @@ -257,6 +259,10 @@ function attributes(p5, fn){ * describe('A small gray square drawn at the center of a white square.'); * } */ + /** + * @method rectMode + * @return {(CENTER|RADIUS|CORNER|CORNERS)} the current rectMode. + */ fn.rectMode = function(m) { // p5._validateParameters('rectMode', arguments); if (typeof m === 'undefined') { // getter From 85d6cccdaf270d617d126d5555cf22592b38aef2 Mon Sep 17 00:00:00 2001 From: dhowe Date: Tue, 24 Mar 2026 17:25:59 +0000 Subject: [PATCH 102/250] updated inline jsdocs for all setters that have them --- src/color/setting.js | 6 ++++++ src/core/environment.js | 6 ++++++ src/image/loading_displaying.js | 6 ++++++ src/shape/attributes.js | 18 ++++++++++++++++++ src/shape/custom_shapes.js | 2 ++ src/webgl/material.js | 13 +++++++++++++ 6 files changed, 51 insertions(+) diff --git a/src/color/setting.js b/src/color/setting.js index 84fab6182c..cae961600b 100644 --- a/src/color/setting.js +++ b/src/color/setting.js @@ -1777,6 +1777,8 @@ function setting(p5, fn){ * EXCLUSION, SCREEN, REPLACE, OVERLAY, HARD_LIGHT, * SOFT_LIGHT, DODGE, BURN, ADD, REMOVE or SUBTRACT * + * Calling `blendMode()` without an argument returns the current blendMode. + * * @example * function setup() { * createCanvas(100, 100); @@ -2145,6 +2147,10 @@ function setting(p5, fn){ * describe('A yellow line and a turquoise line form an X on a gray background. The area where they overlap is green.'); * } */ + /** + * @method blendMode + * @return {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|REMOVE|SUBTRACT)} the current blend mode. + */ fn.blendMode = function (mode) { // p5._validateParameters('blendMode', arguments); if (mode === constants.NORMAL) { diff --git a/src/core/environment.js b/src/core/environment.js index de036e0218..6fffe81973 100644 --- a/src/core/environment.js +++ b/src/core/environment.js @@ -215,6 +215,8 @@ function environment(p5, fn, lifecycles){ * cursor, `x` and `y` set the location pointed to within the image. They are * both 0 by default, so the cursor points to the image's top-left corner. `x` * and `y` must be less than the image's width and height, respectively. + * + * Calling `cursor()` without an argument returns the current cursor type as a string. * * @method cursor * @param {(ARROW|CROSS|HAND|MOVE|TEXT|WAIT|String)} type Built-in: either ARROW, CROSS, HAND, MOVE, TEXT, or WAIT. @@ -281,6 +283,10 @@ function environment(p5, fn, lifecycles){ * } * } */ + /** + * @method cursor + * @return {(ARROW|CROSS|HAND|MOVE|TEXT|WAIT|String)} the current cursor type + */ fn.cursor = function(type, x, y) { let cursor = 'auto'; const canvas = this._curElement.elt; diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 1e068964b7..bc199c1544 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -1325,6 +1325,8 @@ function loadingDisplaying(p5, fn){ * image() as the x- and y-coordinates of the image's * center. The next parameters are its width and height. * + * Calling `imageMode()` without an argument returns the current image mode, either `CORNER`, `CORNERS`, or `CENTER`. + * * @method imageMode * @param {(CORNER|CORNERS|CENTER)} mode either CORNER, CORNERS, or CENTER. * @@ -1388,6 +1390,10 @@ function loadingDisplaying(p5, fn){ * describe('A square image of a brick wall is drawn on a gray square.'); * } */ + /** + * @method imageMode + * @return {(CORNER|CORNERS|CENTER)} the current image mode + */ fn.imageMode = function(m) { // p5._validateParameters('imageMode', arguments); if (typeof m === 'undefined') { // getter diff --git a/src/shape/attributes.js b/src/shape/attributes.js index 77ac58ce0c..812159267f 100644 --- a/src/shape/attributes.js +++ b/src/shape/attributes.js @@ -35,6 +35,8 @@ function attributes(p5, fn){ * the constants `CENTER`, `RADIUS`, `CORNER`, and `CORNERS` are defined this * way. JavaScript is a case-sensitive language. * + * Calling `ellipseMode()` without an argument returns the current ellipseMode, either `CENTER`, `RADIUS`, `CORNER`, or `CORNERS`. + * * @method ellipseMode * @param {(CENTER|RADIUS|CORNER|CORNERS)} mode either CENTER, RADIUS, CORNER, or CORNERS * @chainable @@ -77,6 +79,10 @@ function attributes(p5, fn){ * describe('A white circle with a gray circle at its top-left corner. Both circles have black outlines.'); * } */ + /** + * @method ellipseMode + * @return {(CENTER|RADIUS|CORNER|CORNERS)} the current ellipseMode. + */ fn.ellipseMode = function(m) { // p5._validateParameters('ellipseMode', arguments); if (typeof m === 'undefined') { // getter @@ -416,6 +422,8 @@ function attributes(p5, fn){ * the constants `MITER`, `BEVEL`, and `ROUND` are defined this way. * JavaScript is a case-sensitive language. * + * Calling `strokeJoin()` without an argument returns the current stroke join style, either `MITER`, `BEVEL`, or `ROUND`. + * * @method strokeJoin * @param {(MITER|BEVEL|ROUND)} join either MITER, BEVEL, or ROUND * @chainable @@ -482,6 +490,10 @@ function attributes(p5, fn){ * describe('A right-facing arrowhead shape with a rounded tip in center of canvas.'); * } */ + /** + * @method strokeJoin + * @return {(MITER|BEVEL|ROUND)} the current stroke join style. + */ fn.strokeJoin = function(join) { // p5._validateParameters('strokeJoin', arguments); if (typeof join === 'undefined') { // getter @@ -503,6 +515,8 @@ function attributes(p5, fn){ * * Note: `strokeWeight()` is affected by transformations, especially calls to * scale(). + * + * Calling `strokeWeight()` without an argument returns the current stroke weight as a number. * * @method strokeWeight * @param {Number} weight the weight of the stroke (in pixels). @@ -545,6 +559,10 @@ function attributes(p5, fn){ * describe('Two horizontal black lines. The top line is thin and the bottom is five times thicker than the top.'); * } */ + /** + * @method strokeWeight + * @return {Number} the current stroke weight. + */ fn.strokeWeight = function(w) { // p5._validateParameters('strokeWeight', arguments); return this._renderer.strokeWeight(w); diff --git a/src/shape/custom_shapes.js b/src/shape/custom_shapes.js index 3a09200f75..f287a4823e 100644 --- a/src/shape/custom_shapes.js +++ b/src/shape/custom_shapes.js @@ -1611,6 +1611,8 @@ function customShapes(p5, fn) { * Note: `bezierVertex()` won’t work when an argument is passed to * beginShape(). * + * Calling `bezierOrder()` without an argument returns the current Bézier order. + * * @method bezierOrder * @param {Number} order The new order to set. Can be either 2 or 3, by default 3 * diff --git a/src/webgl/material.js b/src/webgl/material.js index 8b0664eca1..3d0378e26e 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -2652,6 +2652,8 @@ function material(p5, fn) { * * Note: `textureMode()` can only be used in WebGL mode. * + * Calling `textureMode()` with no arguments returns the current texture mode. + * * @method textureMode * @param {(IMAGE|NORMAL)} mode either IMAGE or NORMAL. * @@ -2714,6 +2716,10 @@ function material(p5, fn) { * endShape(); * } */ + /** + * @method textureMode + * @return {(IMAGE|NORMAL)} The current texture mode, either IMAGE or NORMAL. + */ fn.textureMode = function (mode) { if (typeof mode === 'undefined') { // getter return this._renderer.states.textureMode; @@ -2825,6 +2831,9 @@ function material(p5, fn) { * * Note: `textureWrap()` can only be used in WebGL mode. * + * Calling `textureWrap()` with no arguments returns an object with the current + * mode for x and y directions, as in `{ wrapX: CLAMP, wrapY: REPEAT }`. + * * @method textureWrap * @param {(CLAMP|REPEAT|MIRROR)} wrapX either CLAMP, REPEAT, or MIRROR * @param {(CLAMP|REPEAT|MIRROR)} [wrapY=wrapX] either CLAMP, REPEAT, or MIRROR @@ -2980,6 +2989,10 @@ function material(p5, fn) { * endShape(); * } */ + /** + * @method textureWrap + * @return {{x: (CLAMP|REPEAT|MIRROR), y: (CLAMP|REPEAT|MIRROR)}} The current texture wrapping for x and y. + */ fn.textureWrap = function (wrapX, wrapY = wrapX) { if (typeof wrapX === 'undefined') { // getter return { From 1a408afb327b04e1ebedefbabf49225e0ca4067e Mon Sep 17 00:00:00 2001 From: dhowe Date: Tue, 24 Mar 2026 17:27:31 +0000 Subject: [PATCH 103/250] remove no-longer used decomposeMatrix function --- src/math/trigonometry.js | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/math/trigonometry.js b/src/math/trigonometry.js index eb3b20efa9..d2c5491b7d 100644 --- a/src/math/trigonometry.js +++ b/src/math/trigonometry.js @@ -204,33 +204,6 @@ function trigonometry(p5, fn){ return this._fromRadians(Math.asin(ratio)); }; - fn.decomposeMatrix = function(mat) { - // adapted from https://frederic-wang.fr/2013/12/01/decomposition-of-2d-transform-matrices/ - let { a, b, c, d, e, f } = mat; - let delta = a * d - b * c; - let result = { - translation: { x: e, y: f }, - scale: { x: 0, y: 0 }, - shear: { x: 0, y: 0 }, - rotation: 0 - }; - if (a !== 0 || b !== 0) { - let r = Math.sqrt(a * a + b * b); - result.rotation = b > 0 ? Math.acos(a / r) : -Math.acos(a / r); - result.scale = { x: r, y: delta / r }; - result.shear = { x: Math.atan((a * c + b * d) / (r * r)), y: 0 }; - } else if (c !== 0 || d !== 0) { - let s = Math.sqrt(c * c + d * d); - result.rotation = Math.PI / 2 - - (d > 0 ? Math.acos(-c / s) : -Math.acos(c / s)); - result.scale = { x: delta / s, y: s }; - result.shear = { x: 0, y: Math.atan((a * c + b * d) / (s * s)) }; - } else { - // a = b = c = d = 0 - } - return result; - }; - /** * Calculates the arc tangent of a number. * From 0165c12cb93de2b01ec9321ec0b04daee4d095dc Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 24 Mar 2026 19:11:26 -0400 Subject: [PATCH 104/250] Fix bug in storage uniform default values --- src/strands/strands_api.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 1cf150ac0b..3bf2ab2c05 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -501,6 +501,8 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } else if (value && typeof value === 'object' && !value._isStorageBuffer) { // Plain object schema template -- only used to infer struct layout, not as a default value schema = strandsContext.renderer?._inferStructSchema(value) ?? null; + } else if (value?._isStorageBuffer) { + defaultValue = bufferOrSchema; } const { id, dimension } = build.variableNode( From 462d9a6ecd0bc87d6e554f2b918b2dc8dfa729e9 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 24 Mar 2026 19:56:17 -0400 Subject: [PATCH 105/250] off by one.... --- src/webgpu/shaders/compute.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webgpu/shaders/compute.js b/src/webgpu/shaders/compute.js index b6b323a8cd..39e6146f4e 100644 --- a/src/webgpu/shaders/compute.js +++ b/src/webgpu/shaders/compute.js @@ -14,9 +14,9 @@ fn main( var index = vec3(globalId); if ( - index.x > uniforms.uTotalCount.x || - index.y > uniforms.uTotalCount.y || - index.z > uniforms.uTotalCount.z + index.x >= uniforms.uTotalCount.x || + index.y >= uniforms.uTotalCount.y || + index.z >= uniforms.uTotalCount.z ) { return; } From 2008ce8296ecdf2114e105846f9f439e112e08c2 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 24 Mar 2026 20:19:30 -0400 Subject: [PATCH 106/250] Fix uniform storage not updating --- src/webgpu/p5.RendererWebGPU.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 078c46c183..0e98da39ef 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1713,6 +1713,13 @@ function rendererWebGPU(p5, fn) { currentShader.buffersDirty.delete(key); } } + for (const storage of currentShader._storageBuffers || []) { + const key = storage.group * 1000 + storage.binding; + if (currentShader.buffersDirty.has(key)) { + currentShader._cachedBindGroup[storage.group] = undefined; + currentShader.buffersDirty.delete(key); + } + } // Bind sampler/texture uniforms and uniform buffers for (const iter of currentShader._groupEntries) { @@ -2147,7 +2154,7 @@ function rendererWebGPU(p5, fn) { if (uniform.isSampler) { uniform.texture = data instanceof Texture ? data : this.getTexture(data); - } else { + } else if (!data?._isStorageBuffer) { uniform._mappedData = this._mapUniformData(uniform, uniform._cachedData); } shader.buffersDirty.add(uniform.group * 1000 + uniform.binding); From dc43aaefb63d64f1b438deabb715ae90b24c005f Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 24 Mar 2026 20:35:07 -0400 Subject: [PATCH 107/250] Add Game of Life example --- src/core/p5.Renderer3D.js | 154 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index b3edbee96e..60fd6f0f36 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -2225,6 +2225,160 @@ function renderer3D(p5, fn) { * The first parameter, `shader`, is a compute shader created with * `buildComputeShader`. * + * Pass a number for `x` to run a simple loop. Inside the shader's iteration + * function, `index.x` will count up from 0 to + * that number. + * + * ```js example + * let particles; + * let computeShader; + * let displayShader; + * let instance; + * const numParticles = 50; + * + * async function setup() { + * await createCanvas(100, 100, WEBGPU); + * + * let data = []; + * for (let i = 0; i < numParticles; i++) { + * data.push({ + * position: createVector( + * random(-40, 40), + * random(-40, 40) + * ), + * velocity: createVector( + * random(-1, 1), + * random(-1, 1) + * ), + * }); + * } + * particles = createStorage(data); + * + * computeShader = buildComputeShader(simulate); + * displayShader = buildMaterialShader(display); + * instance = buildGeometry(drawParticle); + * } + * + * function drawParticle() { + * sphere(3); + * } + * + * function simulate() { + * let r = 3; + * let data = uniformStorage(particles); + * let idx = index.x; + * let pos = data[idx].position; + * let vel = data[idx].velocity; + * pos = pos + vel; + * if (pos.x > width/2 - r || pos.x < -height/2 + r) { + * vel.x = -vel.x; + * pos.x = clamp(pos.x, -width/2 + r, width/2 - r); + * } + * if (pos.y > height/2 - r || pos.y < -height/2 + r) { + * vel.y = -vel.y; + * pos.y = clamp(pos.y, -height/2 + r, height/2 - r); + * } + * data[idx].position = pos; + * data[idx].velocity = vel; + * } + * + * function display() { + * let data = uniformStorage(particles); + * worldInputs.begin(); + * let pos = data[instanceID()].position; + * worldInputs.position.xy += pos; + * worldInputs.end(); + * } + * + * function draw() { + * background(30); + * compute(computeShader, numParticles); + * noStroke(); + * fill(255); + * lights(); + * shader(displayShader); + * model(instance, numParticles); + * } + * ``` + * + * You can also pass `y` and `z` to loop in up to three dimensions, using + * `index.y` and `index.z` to get the position in each. This is useful for + * working with 2D grids, like in the Game of Life example below. + * + * ```js example + * let cells; + * let nextCells; + * let gameShader; + * let displayShader; + * const W = 100; + * const H = 100; + * + * async function setup() { + * await createCanvas(100, 100, WEBGPU); + * + * let initial = new Float32Array(W * H); + * for (let i = 0; i < initial.length; i++) { + * initial[i] = random() > 0.7 ? 1 : 0; + * } + * cells = createStorage(initial); + * nextCells = createStorage(W * H); + * + * gameShader = buildComputeShader(simulate); + * displayShader = buildFilterShader(display); + * } + * + * function simulate() { + * let current = uniformStorage(() => cells); + * let next = uniformStorage(() => nextCells); + * let w = uniformInt(() => W); + * let h = uniformInt(() => H); + * let x = index.x; + * let y = index.y; + * + * let n = 0; + * for (let dy = -1; dy <= 1; dy++) { + * for (let dx = -1; dx <= 1; dx++) { + * if (dx != 0 || dy != 0) { + * let nx = (x + dx + w) % w; + * let ny = (y + dy + h) % h; + * n += current[ny * w + nx]; + * } + * } + * } + * + * let alive = current[y * w + x]; + * let nextOutput = 0; + * if (alive == 1) { + * if (abs(n - 2) < 0.1 || abs(n - 3) < 0.1) { + * nextOutput = 1; + * } + * } else { + * if (abs(n - 3) < 0.1) { + * nextOutput = 1; + * } + * } + * next[y * w + x] = nextOutput; + * } + * + * function display() { + * let data = uniformStorage(() => cells); + * let w = uniformInt(() => W); + * let h = uniformInt(() => H); + * + * filterColor.begin(); + * let x = floor(filterColor.texCoord.x * w); + * let y = floor(filterColor.texCoord.y * h); + * let alive = data[y * w + x]; + * filterColor.set([alive, alive, alive, 1]); + * filterColor.end(); + * } + * + * function draw() { + * compute(gameShader, W, H); + * [nextCells, cells] = [cells, nextCells]; + * filter(displayShader); + * } + * ``` * * @method compute * @submodule p5.strands From acc319a4253cd59ed399fd3e4c0846f4ce6df99d Mon Sep 17 00:00:00 2001 From: dhowe Date: Wed, 25 Mar 2026 01:18:27 +0000 Subject: [PATCH 108/250] fixes from reviewer --- src/core/p5.Renderer.js | 1 - src/core/p5.Renderer2D.js | 5 ---- src/image/loading_displaying.js | 7 ++--- src/type/textCore.js | 42 ++++++++++++++++----------- test/unit/webgl/p5.RendererGL.js | 49 ++++++++++++++++++-------------- 5 files changed, 55 insertions(+), 49 deletions(-) diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index ce8b1f2b06..f9caac7793 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -35,7 +35,6 @@ class ClonableObject { class Renderer { static states = { - background: null, strokeColor: null, strokeSet: false, fillColor: null, diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 459802a44b..a98f60f339 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -168,9 +168,7 @@ class Renderer2D extends Renderer { background(...args) { if (args.length === 0) { return this;// setter with no args does nothing - //return this.states.background; // getter (#8278) } - let bgForState = null; this.push(); this.resetMatrix(); if (args[0] instanceof Image) { @@ -180,7 +178,6 @@ class Renderer2D extends Renderer { this.drawingContext.globalAlpha = args[1] / 255; } this._pInst.image(img, 0, 0, this.width, this.height); - bgForState = img; // save for getter (#8278) } else { // create background rect const color = this._pInst.color(...args); @@ -203,11 +200,9 @@ class Renderer2D extends Renderer { if (this._isErasing) { this._pInst.erase(); } - bgForState = color; // save for getter (#8278) } this.pop(); - this.states.setValue('background', bgForState); // set state (#8278) return this; } diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index bc199c1544..e15ee286df 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -1138,11 +1138,8 @@ function loadingDisplaying(p5, fn){ * sets the alpha value. For example, `tint(255, 0, 0, 100)` will give images * a red tint and make them transparent. * - * Calling `tint()` without an argument returns the current tint as a p5.Color object. - * - * @method frameRate - * @param {Number} fps number of frames to draw per second. - * @chainable + * Calling `tint()` without an argument returns the current tint as a + * p5.Color object. * * @method tint * @param {Number} v1 red or hue value. diff --git a/src/type/textCore.js b/src/type/textCore.js index 64ceb4d6e1..909609ef0c 100644 --- a/src/type/textCore.js +++ b/src/type/textCore.js @@ -1459,29 +1459,37 @@ function textCore(p5, fn) { Renderer.prototype.textAlign = function (h, v) { - // the setter - if (typeof h !== 'undefined') { - // accept what is returned from the getter + if (arguments.length === 0) { // the getter + return { + horizontal: this.states.textAlign, + vertical: this.states.textBaseline + }; + } + + // allow an object with horizontal and vertical properties + if (typeof h === 'object' && h !== null) { + if (h.hasOwnProperty('vertical')) { + v = h.vertical; + } if (h.hasOwnProperty('horizontal')) { h = h.horizontal; } + } + + // horizontal value as separate argument + if (typeof h === 'string' || h instanceof String) { this.states.setValue('textAlign', h); - if (h.hasOwnProperty('vertical') && typeof v === 'undefined') { - v = h.vertical; - } - if (typeof v !== 'undefined') { - if (v === fn.CENTER) { - v = textCoreConstants._CTX_MIDDLE; - } - this.states.setValue('textBaseline', v); + } + + // vertical value as separate argument + if (typeof v === 'string' || v instanceof String) { + if (v === fn.CENTER) { + v = textCoreConstants._CTX_MIDDLE; } - return this._applyTextProperties(); + this.states.setValue('textBaseline', v); } - // the getter - return { - horizontal: this.states.textAlign, - vertical: this.states.textBaseline - }; + + return this._applyTextProperties(); }; Renderer.prototype._currentTextFont = function () { diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 10976c6b51..4d3eaaf7aa 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1492,46 +1492,52 @@ suite('p5.RendererGL', function() { suite('tint() in WEBGL mode', function() { test('default tint value is set and not null', function() { myp5.createCanvas(100, 100, myp5.WEBGL); - assert.deepEqual(myp5._renderer.states.tint.array(), [1,1,1,1]); + assert.deepEqual(myp5._renderer.states.tint + ._getRGBA([255, 255, 255, 255]), [255, 255, 255, 255]); }); test('tint value is modified correctly when tint() is called', function() { - function assertDeepEqualColor(actual, expected) { - if (typeof actual === 'string' && typeof expected === 'string') { - assert.equal(actual, expected); - } else { - assert.deepEqual(actual, myp5.color(expected).array()); - } + function assertColorEq(tint, colArray) { + assert.deepEqual(tint._getRGBA([255, 255, 255, 255]), colArray); } + myp5.createCanvas(100, 100, myp5.WEBGL); + myp5.tint(0, 153, 204, 126); - assertDeepEqualColor(myp5._renderer.states.tint.array(), [0, 153, 204, 126]); + assertColorEq(myp5._renderer.states.tint, [0, 153, 204, 126]); + myp5.tint(100, 120, 140); - assertDeepEqualColor(myp5._renderer.states.tint.array(), [100, 120, 140, 255]); - + assertColorEq(myp5._renderer.states.tint, [100, 120, 140, 255]); + myp5.tint('violet'); - // Note that in WEBGL mode, we don't convert color strings to arrays until the shader, + // Note that in WEBGL mode, we don't convert color strings to arrays until the shader, // so the tint state is still the string 'violet' at this point, not the array [238, 130, 238, 255]. - //assertDeepEqualColor(myp5._renderer.states.tint.array(), [238, 130, 238, 255]); - assertDeepEqualColor(myp5._renderer.states.tint.array(), 'violet'); + //assertDeepEqualColor(myp5._renderer.states.tint, [238, 130, 238, 255]); + assert.equal(myp5._renderer.states.tint, 'violet'); myp5.tint(100); - assertDeepEqualColor(myp5._renderer.states.tint.array(), [100, 100, 100, 255]); + assertColorEq(myp5._renderer.states.tint, [100, 100, 100, 255]); + myp5.tint(100, 126); - assertDeepEqualColor(myp5._renderer.states.tint.array(), [100, 100, 100, 126]); + assertColorEq(myp5._renderer.states.tint, [100, 100, 100, 126]); + myp5.tint([100, 126, 0, 200]); - assertDeepEqualColor(myp5._renderer.states.tint.array(), [100, 126, 0, 200]); + assertColorEq(myp5._renderer.states.tint, [100, 126, 0, 200]); + myp5.tint([100, 126, 0]); - assertDeepEqualColor(myp5._renderer.states.tint.array(), [100, 126, 0, 255]); + assertColorEq(myp5._renderer.states.tint, [100, 126, 0, 255]); + myp5.tint([100]); - assertDeepEqualColor(myp5._renderer.states.tint.array(), [100, 100, 100, 255]); + assertColorEq(myp5._renderer.states.tint, [100, 100, 100, 255]); + myp5.tint([100, 126]); - assertDeepEqualColor(myp5._renderer.states.tint.array(), [100, 100, 100, 126]); + assertColorEq(myp5._renderer.states.tint, [100, 100, 100, 126]); + myp5.tint(myp5.color(255, 204, 0)); - assertDeepEqualColor(myp5._renderer.states.tint.array(), [255, 204, 0, 255]); + assertColorEq(myp5._renderer.states.tint, [255, 204, 0, 255]); }); test('tint should be reset after draw loop', function() { @@ -1548,7 +1554,8 @@ suite('p5.RendererGL', function() { }; }); }).then(function(_tint) { - assert.deepEqual(_tint.array(), [1, 1, 1, 1]); + assert.deepEqual(_tint._getRGBA([255, 255, 255, 255]), + [255, 255, 255, 255]); }); }); }); From 3d4d45e07895f6638c6b5ecff281a9f5d92543fe Mon Sep 17 00:00:00 2001 From: dhowe Date: Wed, 25 Mar 2026 01:22:08 +0000 Subject: [PATCH 109/250] fix to tint argument handling --- src/image/loading_displaying.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index e15ee286df..28149c4412 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -1253,10 +1253,10 @@ function loadingDisplaying(p5, fn){ if (args.length === 0) { return this._renderer.states.tint; // getter } - if (args && args.length) { + else { this._renderer.states.setValue('tint', this.color(...args)); + return this; } - return this; }; /** From b2c51850dd7e33ab5008fbac201cc61d5570cab0 Mon Sep 17 00:00:00 2001 From: kit Date: Wed, 25 Mar 2026 13:11:00 +0100 Subject: [PATCH 110/250] Vector tests pass --- src/math/math.js | 12 +--- src/math/p5.Vector.js | 105 +++++++++++++++++++++++------------ src/math/patch-vector.js | 28 ++++------ src/webgl/interaction.js | 9 ++- test/unit/math/p5.Vector.js | 78 +++++++++++++++----------- test/unit/webgl/p5.Shader.js | 4 +- 6 files changed, 137 insertions(+), 99 deletions(-) diff --git a/src/math/math.js b/src/math/math.js index 4371585b96..89c44956a1 100644 --- a/src/math/math.js +++ b/src/math/math.js @@ -96,17 +96,7 @@ function math(p5, fn) { * } */ fn.createVector = function (...args) { - - // TODO MOVE PASSING THESE FUNCTIONS TO VECTOR - if (this instanceof p5) { - return new p5.Vector( - this._fromRadians.bind(this), - this._toRadians.bind(this), - ...arguments - ); - } else { - return new p5.Vector(...args); - } + return new p5.Vector(...args); }; /** diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 45a8d9623f..32cf84a98b 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -9,31 +9,44 @@ import * as constants from '../core/constants'; * This function is used by binary vector operations to prioritize shorter vectors, * and to emit a warning when lengths do not match. */ -const smallerDimensionPriority = function(dimOther, dimSelf, args) { - console.log("sDP", args); - const minDimension = Math.min(dimOther, dimSelf); - if (dimOther !== dimSelf) { - console.warn( - `Operating on two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as ${minDimension}D vectors, and any additional values of the longer vector will be ignored.` - ); - } - return minDimension; +const prioritizeSmallerDimension = function(currentVectorDimension, args) { + return Math.min(currentVectorDimension, args.length); + + //if (args.length !== currentVectorDimension && args.length !== 1) { + // TODO how to suppress for valid solo arguments? + // p5._friendlyError( + // `Operating on two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as ${minDimension}D vectors, and any additional values of the longer vector will be ignored.`, 'p5.Vector' + //); + //} + //return minDimension; }; + class Vector { /** - * The values of the N-dimensional vector. + * The values of an N-dimensional vector. * * This array of numbers that represents the vector. * Each number in the array corresponds to a different component of the vector, * like its position in different directions (e.g., x, y, z). - * + * * You can update the values of the entire vector to a new set of values. * You need to provide an array of numbers, where each number represents a component * of the vector (e.g., x, y, z). The length of the array will become the number of * dimensions of the vector. * + * You can add (`add()`), multiply (`mult()`), divide (`div()`), and subtract (`sub()`) + * vectors from each other, and calculate remainder (`rem()`). Only use these functions + * on vectors when they are the same size: for example, both 2D, or both 3D. + * When an operation uses two vectors of different sizes, the smaller dimension will be + * used, any additional values of the longer vector will be ignored. + * + * You can multiply, divide, or calculate remainder of a vector with a single number. Then, + * the same operation will be done on each element of the vector. + * * @type {Array} The array of values representing the vector. + * @throws Will throw an error if provided no arguments, or if the arguments + * are not all finity numbers */ values = []; @@ -41,9 +54,11 @@ class Vector { // This check if the first argument is a function constructor(...args) { - - // not meant to be userfacing so requires valid input - // TODO throw error when no input args + if (args.length === 0) { + p5._friendlyError( + 'Requires valid arguments.', 'p5.Vector' + ); + } if (typeof args[0] === 'function') { this.isPInst = true; @@ -52,9 +67,15 @@ class Vector { args = args.slice(2); } - // todo at this point must be numbers - - this.values = args; + this.values = []; + if(Array.isArray(args) && !args.every(v => typeof v === 'number' && Number.isFinite(v))){ + p5._friendlyError( + 'Arguments contain non-finite numbers', + target.name + ); + } else { + this.values = args; + } } get dimensions(){ @@ -352,8 +373,11 @@ class Vector { * another p5.Vector object, as in `v.add(v2)`, or * an array of numbers, as in `v.add([1, 2, 3])`. * - * If a value isn't provided for a component, it won't change. For - * example, `v.add(4, 5)` adds 4 to `v.x`, 5 to `v.y`, and 0 to `v.z`. + * Add vectors only when they are the same size: both 2D, or both 3D. + * When two vectors of different sizes are added, the smaller dimension will be + * used, any additional values of the longer vector will be ignored. + * For example, adding `[1, 2, 3]` and `[4, 5]` will result in `[5, 7]`. + * * Calling `add()` with no arguments, as in `v.add()`, has no effect. * * This method supports N-dimensional vectors. @@ -470,7 +494,7 @@ class Vector { * @chainable */ add(...args) { - const minDimension = smallerDimensionPriority(args.length, this.dimensions, args); + const minDimension = prioritizeSmallerDimension(this.dimensions, args); this.values = this.values.reduce((acc, v, i) => { if(i < minDimension) acc[i] = this.values[i] + Number(args[i]); @@ -491,10 +515,14 @@ class Vector { * an array of numbers, as in `v.rem([1, 2, 3])`. * * If only one value is provided, as in `v.rem(2)`, then all the components - * will be set to their values modulo 2. If two values are provided, as in - * `v.rem(2, 3)`, then `v.z` won't change. Calling `rem()` with no + * will be set to their values modulo 2. Calling `rem()` with no * arguments, as in `v.rem()`, has no effect. * + * Modulo vectors only when they are the same size: both 2D, or both 3D. + * When two vectors of different sizes are used, the smaller dimension will be + * used, any additional values of the longer vector will be ignored. + * For example, taking `[3, 6, 9]` modulo `[2, 4]` will result in `[1, 2]`. + * * The static version of `rem()`, as in `p5.Vector.rem(v2, v1)`, returns a * new p5.Vector object and doesn't change the * originals. @@ -590,7 +618,7 @@ class Vector { * @chainable */ rem(...args) { - const minDimension = smallerDimensionPriority(args.length, this.dimensions, args); + const minDimension = prioritizeSmallerDimension(this.dimensions, args); this.values = Array.from({ length: minDimension }, (_, i) => { return (args[i] > 0) ? this.values[i] % args[i] : this.values[i]; @@ -606,10 +634,13 @@ class Vector { * p5.Vector object, as in `v.sub(v2)`, or an array * of numbers, as in `v.sub([1, 2, 3])`. * - * If a value isn't provided for a component, it won't change. For - * example, `v.sub(4, 5)` subtracts 4 from `v.x`, 5 from `v.y`, and 0 from `v.z`. * Calling `sub()` with no arguments, as in `v.sub()`, has no effect. * + * Subtract vectors only when they are the same size: both 2D, or both 3D. + * When two vectors of different sizes are used, the smaller dimension will be + * used, any additional values of the longer vector will be ignored. + * For example, subtracting `[1, 2]` from `[3, 5, 7]` will result in `[2, 3]`. + * * The static version of `sub()`, as in `p5.Vector.sub(v2, v1)`, returns a new * p5.Vector object and doesn't change the * originals. @@ -719,7 +750,7 @@ class Vector { * @chainable */ sub(...args) { - const minDimension = smallerDimensionPriority(args.length, this.dimensions, args); + const minDimension = prioritizeSmallerDimension(this.dimensions, args); this.values = this.values.reduce((acc, v, i) => { if(i < minDimension) acc[i] = this.values[i] - args[i]; @@ -737,11 +768,14 @@ class Vector { * of numbers, as in `v.mult([1, 2, 3])`. * * If only one value is provided, as in `v.mult(2)`, then all the components - * will be multiplied by 2. If a value isn't provided for a component, it - * won't change. For example, `v.mult(4, 5)` multiplies `v.x` by, `v.y` by 5, - * and `v.z` by 1. Calling `mult()` with no arguments, as in `v.mult()`, has + * will be multiplied by 2. Calling `mult()` with no arguments, as in `v.mult()`, has * no effect. * + * Multiply vectors only when they are the same size: both 2D, or both 3D. + * When two vectors of different sizes are multiplied, the smaller dimension will be + * used, any additional values of the longer vector will be ignored. + * For example, multiplying `[1, 2, 3]` by `[4, 5]` will result in `[4, 10]`. + * * The static version of `mult()`, as in `p5.Vector.mult(v, 2)`, returns a new * p5.Vector object and doesn't change the * originals. @@ -904,7 +938,7 @@ class Vector { * @chainable */ mult(...args) { - const minDimension = smallerDimensionPriority(args.length, this.dimensions, args); + const minDimension = prioritizeSmallerDimension(this.dimensions, args); this.values = this.values.reduce((acc, v, i) => { if(i < minDimension) acc[i] = this.values[i] * args[i]; @@ -960,11 +994,14 @@ class Vector { * of numbers, as in `v.div([1, 2, 3])`. * * If only one value is provided, as in `v.div(2)`, then all the components - * will be divided by 2. If a value isn't provided for a component, it - * won't change. For example, `v.div(4, 5)` divides `v.x` by, `v.y` by 5, - * and `v.z` by 1. Calling `div()` with no arguments, as in `v.div()`, has + * will be divided by 2. Calling `div()` with no arguments, as in `v.div()`, has * no effect. * + * Divide vectors only when they are the same size: both 2D, or both 3D. + * When two vectors of different sizes are divided, the smaller dimension will be + * used, any additional values of the longer vector will be ignored. + * For example, dividing `[8, 12, 21]` by `[2, 3]` will result in `[4, 4]`. + * * The static version of `div()`, as in `p5.Vector.div(v, 2)`, returns a new * p5.Vector object and doesn't change the * originals. @@ -1128,7 +1165,7 @@ class Vector { * @chainable */ div(...args) { - const minDimension = smallerDimensionPriority(args.length, this.dimensions, args); + const minDimension = prioritizeSmallerDimension(this.dimensions, args); if(!args.every(v => typeof v === 'number' && v !== 0)){ console.warn( diff --git a/src/math/patch-vector.js b/src/math/patch-vector.js index cd5b8968e9..8216c822e0 100644 --- a/src/math/patch-vector.js +++ b/src/math/patch-vector.js @@ -29,23 +29,19 @@ export function _defaultEmptyVector(target){ export function _validatedVectorOperation(expectsSoloNumberArgument){ return function(target){ return function(...args){ - console.log("vVO", target.name, args); if (args.length === 0) { // No arguments? No action return this; } else if (args[0] instanceof Vector) { + // First argument is a vector? Make it an array args = args[0].values; } else if (Array.isArray(args[0])) { + // First argument is an array? Great, keep it! args = args[0]; - } else if (args.length === 1) { - console.log("A") - if (expectsSoloNumberArgument){ - console.log("b") - // && typeof args[0] === 'number' && Number.isFinite(args[0]) - // Special case handling for a solo numeric argument - args = new Array(3).fill(args[0]); - } - } // (1,2,3) ...args is 1,2,3 + } else if (expectsSoloNumberArgument && args.length === 1){ + // Special case for a solo numeric arguments only applies sometimes + args = new Array(3).fill(args[0]); + } if(Array.isArray(args) && !args.every(v => typeof v === 'number' && Number.isFinite(v))){ p5._friendlyError( @@ -69,13 +65,9 @@ export default function vectorValidation(p5, fn, lifecycles){ p5.registerDecorator('p5.prototype.createVector', _defaultEmptyVector); p5.registerDecorator('p5.Vector.prototype.mult', _validatedVectorOperation(true)); - - p5.registerDecorator(function(path){ - return ['p5.Vector.prototype.add', 'p5.Vector.prototype.sub'].includes(path); - }, _validatedVectorOperation(false)); - - p5.registerDecorator(function(path){ - return ['p5.Vector.prototype.rem', 'p5.Vector.prototype.div'].includes(path); - }, _validatedVectorOperation(true)); + p5.registerDecorator('p5.Vector.prototype.rem', _validatedVectorOperation(true)); + p5.registerDecorator('p5.Vector.prototype.div', _validatedVectorOperation(true)); + p5.registerDecorator('p5.Vector.prototype.add', _validatedVectorOperation(false)); + p5.registerDecorator('p5.Vector.prototype.sub', _validatedVectorOperation(false)); } diff --git a/src/webgl/interaction.js b/src/webgl/interaction.js index ef5beb665a..dd09abed1e 100644 --- a/src/webgl/interaction.js +++ b/src/webgl/interaction.js @@ -370,8 +370,11 @@ function interaction(p5, fn){ // accelerate rotate velocity this._renderer.rotateVelocity.add( deltaTheta * rotateAccelerationFactor, - deltaPhi * rotateAccelerationFactor + deltaPhi * rotateAccelerationFactor, + 0 ); + + //console.log("added"); } if (this._renderer.rotateVelocity.magSq() > 0.000001) { // if freeRotation is true, the camera always rotates freely in the direction the pointer moves @@ -390,8 +393,10 @@ function interaction(p5, fn){ } // damping this._renderer.rotateVelocity.mult(damping); + //console.log("multiplied", damping, this._renderer.rotateVelocity); + } else { - this._renderer.rotateVelocity.set(0, 0); + this._renderer.rotateVelocity.set(0, 0, 0); } // move process diff --git a/test/unit/math/p5.Vector.js b/test/unit/math/p5.Vector.js index 537ff1a110..d0fe08bce5 100644 --- a/test/unit/math/p5.Vector.js +++ b/test/unit/math/p5.Vector.js @@ -21,14 +21,16 @@ suite('p5.Vector', function () { ); // The following mocks simulate the validation decorator - Vector.prototype.add = _validatedVectorOperation(Vector.prototype.add); - Vector.prototype.sub = _validatedVectorOperation(Vector.prototype.sub); - Vector.prototype.mult = _validatedVectorOperationtk(Vector.prototype.mult); - Vector.prototype.rem = _validatedVectorOperationtk(Vector.prototype.rem); - Vector.prototype.div = _validatedVectorOperationjt(Vector.prototype.div); + Vector.prototype.add = _validatedVectorOperation(false)(Vector.prototype.add); + Vector.prototype.sub = _validatedVectorOperation(false)(Vector.prototype.sub); + Vector.prototype.mult = _validatedVectorOperation(true)(Vector.prototype.mult); + Vector.prototype.rem = _validatedVectorOperation(true)(Vector.prototype.rem); + Vector.prototype.div = _validatedVectorOperation(true)(Vector.prototype.div); + globalThis.FESCalled = false; globalThis.p5 = { _friendlyError: function(msg, func) { + globalThis.FESCalled = true; console.warn(msg); } }; @@ -100,7 +102,7 @@ suite('p5.Vector', function () { }); - suite('p5.prototype.createVector()', function () { + suite.todo('p5.prototype.createVector()', function () { beforeEach(function () { v = mockP5Prototype.createVector(); }); @@ -148,9 +150,9 @@ suite('p5.Vector', function () { }); }); - suite('new p5.Vector(1,2,undefined)', function () { + suite('new p5.Vector(1,2)', function () { beforeEach(function () { - v = new Vector(1, 2, undefined); + v = new Vector(1, 2); }); test('should have x, y, z be initialized to 1,2,0', function () { @@ -542,10 +544,12 @@ suite('p5.Vector', function () { }); test('should give correct output if passed two numeric value', function () { + expect(v.dimensions).to.eql(3); v.rem(2, 3); expect(v.x).to.eql(1); expect(v.y).to.eql(1); - expect(v.z).to.eql(5); + expect(v.z).to.eql(0); + expect(v.dimensions).to.eql(2); }); test('should give correct output if passed three numeric value', function () { @@ -609,14 +613,16 @@ suite('p5.Vector', function () { v.rem([2, 3]); expect(v.x).to.eql(1); expect(v.y).to.eql(1); - expect(v.z).to.eql(5); + expect(v.z).to.eql(0); + expect(v.dimensions).to.eql(2); }); test('should return correct output if x,y components are zero for 2D vector', () => { v.rem([0, 0]); expect(v.x).to.eql(3); expect(v.y).to.eql(4); - expect(v.z).to.eql(5); + expect(v.z).to.eql(0); + expect(v.dimensions).to.eql(2); }); test('should return same vector if any vector component is non-finite number', () => { @@ -1013,47 +1019,51 @@ suite('p5.Vector', function () { suite('smaller dimension', function () { - let v0, v1, v2, v3; + let v1, v2, v3; beforeEach(function () { - v0 = new Vector(); - v1 = new Vector([1]); - v2 = new Vector([2, 3]); - v3 = new Vector([4, 5, 6]); + v1 = new Vector(1); + v2 = new Vector(2, 3); + v3 = new Vector(4, 5, 6); }); test('should be prioritized in add()', function () { - assert.deepEqual(v1.add(v2).values, [2]); - assert.deepEqual(v1.add(v2).dimension, 1); - assert.deepEqual(v3.add(v2).values, [8,15]); - assert.deepEqual(v3.add(v2).dimension, 2); + assert.deepEqual(v1.add(v2).values, [3]); + expect(v1.add(v2).dimensions).to.eql(1); + + assert.deepEqual(v3.add(v2).values, [6, 8]); + expect(v3.add(v2).dimensions).to.eql(2); }); test('should be prioritized in sub()', function () { assert.deepEqual(v1.sub(v2).values, [-1]); - assert.deepEqual(v1.sub(v2).dimension, 1); + expect(v1.sub(v2).dimensions).to.eql(1); + assert.deepEqual(v3.sub(v2).values, [2, 2]); - assert.deepEqual(v3.sub(v2).dimension, 2); + expect(v3.sub(v2).dimensions).to.eql(2); }); test('should be prioritized in mult()', function () { assert.deepEqual(v1.mult(v2).values, [2]); - assert.deepEqual(v1.mult(v2).dimension, 1); + expect(v1.mult(v2).dimensions).to.eql(1); + assert.deepEqual(v3.mult(v2).values, [8, 15]); - assert.deepEqual(v3.mult(v2).dimension, 2); + expect(v3.mult(v2).dimensions).to.eql(2); }); test('should be prioritized in div()', function () { assert.deepEqual(v1.div(v2).values, [1/2]); - assert.deepEqual(v1.div(v2).dimension, 1); + expect(v1.div(v2).dimensions).to.eql(1); + assert.deepEqual(v3.div(v2).values, [2, 5/3]); - assert.deepEqual(v3.div(v2).dimension, 2); + expect(v3.div(v2).dimensions).to.eql(2); }); test('should be prioritized in rem()', function () { assert.deepEqual(v1.rem(v2).values, [1]); - assert.deepEqual(v1.rem(v2).dimension, 1); + expect(v1.rem(v2).dimensions).to.eql(1); + assert.deepEqual(v3.rem(v2).values, [0, 2]); - assert.deepEqual(v3.rem(v2).dimension, 2); + expect(v3.rem(v2).dimensions).to.eql(2); }); }); @@ -2078,11 +2088,12 @@ suite('p5.Vector', function () { assert.equal(vect.getValue(3), 4); }); - test.fails( - 'should throw friendly error if attempting to get element outside lenght', + test('should throw friendly error if attempting to get element outside length', function () { let vect = new Vector(1, 2, 3, 4); - assert.equal(vect.getValue(5), 1); + globalThis.FESCalled = false; + assert.equal(vect.getValue(5), undefined); + assert.equal(globalThis.FESCalled, true); } ); }); @@ -2097,11 +2108,12 @@ suite('p5.Vector', function () { assert.equal(vect.getValue(3), 4); }); - test.fails( - 'should throw friendly error if attempting to set element outside lenght', + test('should throw friendly error if attempting to set element outside lenght', function () { let vect = new Vector(1, 2, 3, 4); + globalThis.FESCalled = false; vect.setValue(100, 7); + assert.equal(globalThis.FESCalled, true); } ); }); diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 6241c88973..c1c45039d7 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -2091,7 +2091,9 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca }, { myp5 }); } catch (e) { /* expected */ } - assert.isAbove(mockUserError.mock.calls.length, 0, 'FES.userError should have been called'); + + + assert.isAbove(mockUserError.mock.calls.length, 0, 'FES.userError should have been called, btw: '+globalThis.FESCalled); const errMsg = mockUserError.mock.calls[0][1]; assert.include(errMsg, 'float3'); assert.include(errMsg, 'float4'); From 719b4bd62b9522fe36335cfe575b04ad7738593b Mon Sep 17 00:00:00 2001 From: kit Date: Wed, 25 Mar 2026 14:30:18 +0100 Subject: [PATCH 111/250] Update createVector doc --- src/math/math.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/math/math.js b/src/math/math.js index 89c44956a1..0cfbb593bb 100644 --- a/src/math/math.js +++ b/src/math/math.js @@ -6,9 +6,6 @@ function math(p5, fn) { /** - * - * Testing - * * Creates a new p5.Vector object. * * A vector can be thought of in different ways. In one view, a vector is like @@ -41,7 +38,7 @@ function math(p5, fn) { * p5.Vector class. * * @method createVector - * @param {...Number} x Zero or more numbers, representing each component of the vector. + * @param {...Number} x List of numbers representing each component of the vector. * @return {p5.Vector} new p5.Vector object. * * @example From 32e351a15d422ef5ca052ee114e29716dfe0ae29 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 25 Mar 2026 17:55:58 +0000 Subject: [PATCH 112/250] Prevent minification mangling from messing up decoration --- src/core/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/main.js b/src/core/main.js index 4ce9d91c55..969c3a2ba1 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -50,7 +50,7 @@ class p5 { constructor(sketch, node) { // Apply addon defined decorations if(p5.decorations.size > 0){ - decorateClass(p5, p5.decorations); + decorateClass(p5, p5.decorations, 'p5'); p5.decorations.clear(); } From 4d264b46f2645012585a7952fba0eab5b994df2a Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 25 Mar 2026 16:53:10 -0400 Subject: [PATCH 113/250] Finish handling for color and vector uniforms; fix bug when directly setting node ID 0 --- src/color/p5.Color.js | 9 +++-- src/strands/strands_codegen.js | 2 +- src/webgl/p5.Shader.js | 14 +++++--- src/webgpu/p5.RendererWebGPU.js | 8 ++++- test/unit/visual/cases/webgl.js | 33 ++++++++++++++++++ .../000.png | Bin 0 -> 527 bytes .../metadata.json | 3 ++ .../000.png | Bin 0 -> 472 bytes .../metadata.json | 3 ++ 9 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/setUniform with p5.Color sets final color/000.png create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/setUniform with p5.Color sets final color/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/setUniform with p5.Vector offsets position/000.png create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/setUniform with p5.Vector offsets position/metadata.json diff --git a/src/color/p5.Color.js b/src/color/p5.Color.js index cd7561b939..592682639b 100644 --- a/src/color/p5.Color.js +++ b/src/color/p5.Color.js @@ -61,6 +61,9 @@ class Color { static #colorjsMaxes = {}; static #grayscaleMap = {}; + // For duck typing + isColor = true; + // Used to add additional color modes to p5.js // Uses underlying library's definition static addColorMode(mode, definition){ @@ -364,7 +367,7 @@ class Color { if (format === undefined && this._defaultStringValue !== undefined) { return this._defaultStringValue; } - + let outputFormat = format; if (format === '#rrggbb') { outputFormat = 'hex'; @@ -377,10 +380,10 @@ class Color { colorString = serialize(this._color, { format: outputFormat }); - + if (format === '#rrggbb') { colorString = String(colorString); - if (colorString.length === 4) { + if (colorString.length === 4) { const r = colorString[1]; const g = colorString[2]; const b = colorString[3]; diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index d09a00e812..89e1aed22a 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -73,7 +73,7 @@ export function generateShaderCode(strandsContext) { returnType = hookType.returnType.dataType; } - if (rootNodeID) { + if (rootNodeID !== undefined) { backend.generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType); } hooksObj[`${hookType.returnType.typeName} ${hookType.name}`] = [firstLine, ...generationContext.codeLines, '}'].join('\n'); diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 0cbca35397..a16f6b8907 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -253,15 +253,15 @@ class Shader { * type of the data, such as `uniformFloat` for a number or `uniformVector2` for a two-component vector. * They take in a function that returns the data for the variable. You can then reference these * variables in your hooks, and their values will update every time you apply - * the shader with the result of your function. - * + * the shader with the result of your function. + * * Move the mouse over this sketch to increase the moveCounter which will be passed to the shader as a uniform. * * ```js example * let myShader; * //count of frames in which mouse has been moved * let moveCounter = 0; - * + * * function setup() { * createCanvas(200, 200, WEBGL); * myShader = baseMaterialShader().modify(() => { @@ -888,7 +888,7 @@ class Shader { * @chainable * @param {String} uniformName name of the uniform. Must match the name * used in the vertex and fragment shaders. - * @param {Boolean|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture|p5.StorageBuffer} + * @param {Boolean|p5.Vector|p5.Color|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture|p5.StorageBuffer} * data value to assign to the uniform. Must match the uniform’s data type. * * @example @@ -1099,6 +1099,12 @@ class Shader { return; } + if (data?.isVector) { + data = data.values.length !== data.dimensions ? data.values.slice(0, data.dimensions) : data.values; + } else if (data?.isColor) { + data = data._getRGBA([1, 1, 1, 1]); + } + if (uniform.isArray) { if ( uniform._cachedData && diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 0e98da39ef..d71054111a 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1815,6 +1815,8 @@ function rendererWebGPU(p5, fn) { if (value === undefined) return; if (value?.isVector) { value = value.values.length !== value.dimensions ? value.values.slice(0, value.dimensions) : value.values; + } else if (value?.isColor) { + value = value._getRGBA([1, 1, 1, 1]); } const byteOffset = baseOffset + field.offset; if (field.baseType === 'u32') { @@ -3061,6 +3063,9 @@ ${hookUniformFields}} if (value.dimensions === 4) return 'vec4f'; throw new Error(`Unsupported vector dimension ${value.dimensions} for struct storage field`); } + if (value?.isColor) { + return 'vec4f'; + } if (Array.isArray(value)) { if (value.length === 2) return 'vec2f'; if (value.length === 3) return 'vec3f'; @@ -3087,7 +3092,8 @@ ${hookUniformFields}} value !== null && typeof value === 'object' && !Array.isArray(value) && - !value?.isVector + !value?.isVector && + !value?.isColor ) { p5._friendlyError( `The "${name}" property in your storage data contains a nested object. ` + diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 1893e40252..5eb56bae53 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -1293,6 +1293,39 @@ visualSuite('WebGL', function() { screenshot(); }); + + visualTest('setUniform with p5.Vector offsets position', (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const myShader = p5.baseMaterialShader().modify(() => { + const uOffset = p5.uniformVec2('uOffset'); + p5.worldInputs.begin(); + p5.worldInputs.position.xy += uOffset; + p5.worldInputs.end(); + }, { p5 }); + p5.background(200); + p5.shader(myShader); + myShader.setUniform('uOffset', p5.createVector(10, -10)); + p5.noStroke(); + p5.fill('red'); + p5.circle(0, 0, 20); + screenshot(); + }); + + visualTest('setUniform with p5.Color sets final color', (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const myShader = p5.baseMaterialShader().modify(() => { + const uColor = p5.uniformVec4('uColor'); + p5.finalColor.begin(); + p5.finalColor.set(uColor); + p5.finalColor.end(); + }, { p5 }); + p5.background(200); + p5.shader(myShader); + myShader.setUniform('uColor', p5.color(0, 100, 200)); + p5.noStroke(); + p5.circle(0, 0, 30); + screenshot(); + }); }); visualSuite('background()', function () { diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/setUniform with p5.Color sets final color/000.png b/test/unit/visual/screenshots/WebGL/p5.strands/setUniform with p5.Color sets final color/000.png new file mode 100644 index 0000000000000000000000000000000000000000..ada712c7c54a291e05a33a679581fac1de319601 GIT binary patch literal 527 zcmV+q0`UEbP)_$*b~9VJ@qABfVKGyzcioZW%Q)jOh+;Y( zKJVnNL6qVZNZzBk?L;@w?cKkt!yV!3>B#^8ks524AanvLC6hofRx8CFRgoAWkVv3W z7`)YP;4Oo2iC~C%rtm}&&kzWK2#ArtbZcH)&A%I%7XZ&0ifP1HbxFG<*v?6H)1mcXJ?~L+tI*6P> z(uD#^DrA~XbtJ8nNk-L?q(Y|IR7cWEnPl_>00960%7dJc00006Nkl&^fG literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/setUniform with p5.Color sets final color/metadata.json b/test/unit/visual/screenshots/WebGL/p5.strands/setUniform with p5.Color sets final color/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/p5.strands/setUniform with p5.Color sets final color/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/setUniform with p5.Vector offsets position/000.png b/test/unit/visual/screenshots/WebGL/p5.strands/setUniform with p5.Vector offsets position/000.png new file mode 100644 index 0000000000000000000000000000000000000000..eee8dd8f2861246d577c451be726f5cc47a4f5de GIT binary patch literal 472 zcmV;}0Vn>6P)SF9pGn7h-6f9(sp&muDFW+r`kO0q_6xPQl1Prc6X&tXc^OX&}o) zM{}4roD4A5$~o{b(x52)809mh<8(0)`i7aV>*6A7=ZaYH83c){W4SChy$00030|E!qKn*aa+21!IgR09Abq8@UzQ_X(> O0000 Date: Wed, 25 Mar 2026 17:30:16 -0400 Subject: [PATCH 114/250] Add instance ID docs, add missing beta tags --- src/strands/p5.strands.js | 144 +++++++++++++++++++++++++++++++------- 1 file changed, 117 insertions(+), 27 deletions(-) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 23000e4988..5efec09cbf 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -215,6 +215,7 @@ if (typeof p5 !== "undefined") { /* ------------------------------------------------------------- */ /** * @property {Object} worldInputs + * @beta * @description * A shader hook block that modifies the world-space properties of each vertex in a shader. This hook can be used inside `buildColorShader()` and similar shader `modify()` calls to customize vertex positions, normals, texture coordinates, and colors before rendering. Modifications happen between the `.begin()` and `.end()` methods of the hook. "World space" refers to the coordinate system of the 3D scene, before any camera or projection transformations are applied. * @@ -259,6 +260,7 @@ if (typeof p5 !== "undefined") { /** * @property {Object} combineColors + * @beta * @description * A shader hook block that modifies how color components are combined in the fragment shader. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to control the final color output of a material. Modifications happen between the `.begin()` and `.end()` methods of the hook. * @@ -309,8 +311,115 @@ if (typeof p5 !== "undefined") { * } */ +/** + * @method instanceID + * @beta + * @description + * Returns the index of the current instance when drawing multiple copies of a + * shape with `model(count)`. The first instance has an + * ID of `0`, the second has `1`, and so on. + * + * This lets each copy of a shape behave differently. For example, you can use + * the ID to place instances at different positions, give them different colors, + * or animate them at different speeds. + * + * `instanceID()` can only be used inside a p5.strands shader callback. + * + * ```js example + * let instancesShader; + * let instance; + * let count = 5; + * + * function drawInstance() { + * sphere(15); + * } + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * instance = buildGeometry(drawInstance); + * instancesShader = buildMaterialShader(drawSpaced); + * } + * + * function drawSpaced() { + * worldInputs.begin(); + * // Spread spheres evenly across the canvas based on their index + * let spacing = width / count; + * worldInputs.position.x += (instanceID() - (count - 1) / 2) * spacing; + * worldInputs.end(); + * } + * + * function draw() { + * background(220); + * lights(); + * noStroke(); + * fill('red'); + * shader(instancesShader); + * model(instance, count); + * } + * ``` + * + * If you are using WebGPU mode, a common pattern is to use `instanceID()` to look up data made with + * `createStorage()`. + * This lets you give each instance different properties. + * + * ```js example + * let instanceData; + * let instancesShader; + * let instance; + * let count = 5; + * + * async function setup() { + * await createCanvas(200, 200, WEBGPU); + * + * let data = []; + * for (let i = 0; i < count; i++) { + * data.push({ + * position: createVector((i - (count - 1) / 2) * 40, 0, 0), + * color: color(random(255), random(255), random(255)), + * }); + * } + * instanceData = createStorage(data); + * instance = buildGeometry(drawInstance); + * instancesShader = buildMaterialShader(drawInstances); + * } + * + * function drawInstance() { + * sphere(15); + * } + * + * function drawInstances() { + * let data = uniformStorage(instanceData); + * let itemColor = sharedVec4(); + * + * worldInputs.begin(); + * let item = data[instanceID()]; + * itemColor = item.color; + * worldInputs.position += item.position; + * worldInputs.end(); + * + * finalColor.begin(); + * finalColor.set(itemColor); + * finalColor.end(); + * } + * + * function draw() { + * background(220); + * lights(); + * noStroke(); + * shader(instancesShader); + * model(instance, count); + * } + * ``` + * + * This can be paired with `buildComputeShader` + * to update the data being read. + * + * @returns {*} The index of the current instance. + */ + /** * @method smoothstep + * @beta * @description * A shader function that performs smooth Hermite interpolation between `0.0` * and `1.0`. @@ -472,6 +581,7 @@ if (typeof p5 !== "undefined") { /** * @property {Object} pixelInputs + * @beta * @description * A shader hook block that modifies the properties of each pixel before the final color is calculated. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to adjust per-pixel data before lighting is applied. Modifications happen between the `.begin()` and `.end()` methods of the hook. * @@ -559,6 +669,7 @@ if (typeof p5 !== "undefined") { /** * @property finalColor + * @beta * @description * A shader hook block that modifies the final color of each pixel after all lighting is applied. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to adjust the color before it appears on the screen. Modifications happen between the `.begin()` and `.end()` methods of the hook. * @@ -767,33 +878,6 @@ if (typeof p5 !== "undefined") { * } */ -/** - * Retrieves the current color of a given texture at given coordinates. - * - * The given coordinates should be between [0, 0] representing the top-left of - * the texture, and [1, 1] representing the bottom-right of the texture. - * - * The given texture could be, for example: - * * p5.Image, - * * a p5.Graphics, or - * * a p5.Framebuffer. - * - * The retrieved color that is returned will behave like a vec4, with components - * for red, green, blue, and alpha, each between 0.0 and 1.0. - * - * Linear interpolation is used by default. For Framebuffer sources, you can - * prevent this by creating the buffer with: - * ```js - * createFramebuffer({ - * textureFiltering: NEAREST - * }) - * ``` - * This can be useful if you are using your texture to store data other than color. - * See createFramebuffer. - * - * Note: The `getTexture` function is only available when using p5.strands. - */ - /** * Declares a storage buffer uniform inside a modify() callback, * making a createStorage() buffer accessible in the shader. @@ -935,30 +1019,36 @@ if (typeof p5 !== "undefined") { /** * @method getWorldInputs + * @beta * @param {Function} callback */ /** * @method getPixelInputs + * @beta * @param {Function} callback */ /** * @method getFinalColor + * @beta * @param {Function} callback */ /** * @method getColor + * @beta * @param {Function} callback */ /** * @method getObjectInputs + * @beta * @param {Function} callback */ /** * @method getCameraInputs + * @beta * @param {Function} callback */ From f54ec95437b42aa0a3654ffdbe4b1eb4a518a04f Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 25 Mar 2026 17:32:08 -0400 Subject: [PATCH 115/250] Add webgpu tag to get example working --- src/strands/p5.strands.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 5efec09cbf..8efdd64913 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -414,6 +414,7 @@ if (typeof p5 !== "undefined") { * This can be paired with `buildComputeShader` * to update the data being read. * + * @webgpu * @returns {*} The index of the current instance. */ From c61618d0000c0b4f8707c9b89265b3a4f7d43a65 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 25 Mar 2026 18:09:27 -0400 Subject: [PATCH 116/250] Document createStorage --- src/core/p5.Renderer3D.js | 151 +++++++++++++++++++++++++++++++++++++- src/strands/p5.strands.js | 15 +++- src/webgl/loading.js | 59 +++++++++++++-- 3 files changed, 213 insertions(+), 12 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 60fd6f0f36..6ca6719f6f 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -1996,7 +1996,156 @@ function renderer3D(p5, fn) { p5.Renderer3D = Renderer3D; /** - * Creates a storage buffer for use in compute shaders. + * Creates a `p5.StorageBuffer`, which is + * a block of data that shaders can read from, and compute shaders + * can also write to. This is only available in WebGPU mode. + * + * To read or write the data inside a shader, use + * `uniformStorage()`. To update its contents + * from JavaScript, call `.update()` + * on the result with new data. + * + * Pass an array of objects to store a list of items, each with named + * properties. The properties can be numbers, arrays of numbers, vectors + * created with `createVector()`, or colors + * created with `color()`. Inside the shader, each + * item is accessed by index, and its properties are available by name. + * + * ```js example + * let instanceData; + * let instancesShader; + * let instance; + * let count = 5; + * + * async function setup() { + * await createCanvas(200, 200, WEBGPU); + * + * let data = []; + * for (let i = 0; i < count; i++) { + * data.push({ + * position: createVector( + * random(-1, 1) * width / 2, + * random(-1, 1) * height / 2, + * 0, + * ), + * color: color( + * random(255), + * random(255), + * random(255) + * ) + * }); + * } + * instanceData = createStorage(data); + * instance = buildGeometry(drawInstance); + * instancesShader = buildMaterialShader(drawInstances); + * } + * + * function drawInstance() { + * sphere(15); + * } + * + * function drawInstances() { + * let data = uniformStorage(instanceData); + * let itemColor = sharedVec4(); + * + * worldInputs.begin(); + * let item = data[instanceID()]; + * itemColor = item.color; + * worldInputs.position += item.position; + * worldInputs.end(); + * + * finalColor.begin(); + * finalColor.set(itemColor); + * finalColor.end(); + * } + * + * function draw() { + * background(220); + * lights(); + * noStroke(); + * shader(instancesShader); + * model(instance, count); + * } + * ``` + * + * You can also store a plain list of numbers by passing an array of numbers. + * Inside the shader, each number is accessed by index directly. To create an + * empty list to be filled in by a compute shader, pass a count instead. + * + * ```js example + * let cells; + * let nextCells; + * let gameShader; + * let displayShader; + * const W = 100; + * const H = 100; + * + * async function setup() { + * await createCanvas(100, 100, WEBGPU); + * + * let initial = new Float32Array(W * H); + * for (let i = 0; i < initial.length; i++) { + * initial[i] = random() > 0.7 ? 1 : 0; + * } + * cells = createStorage(initial); + * nextCells = createStorage(W * H); + * + * gameShader = buildComputeShader(simulate); + * displayShader = buildFilterShader(display); + * } + * + * function simulate() { + * let current = uniformStorage(() => cells); + * let next = uniformStorage(() => nextCells); + * let w = uniformInt(() => W); + * let h = uniformInt(() => H); + * let x = index.x; + * let y = index.y; + * + * let n = 0; + * for (let dy = -1; dy <= 1; dy++) { + * for (let dx = -1; dx <= 1; dx++) { + * if (dx != 0 || dy != 0) { + * let nx = (x + dx + w) % w; + * let ny = (y + dy + h) % h; + * n += current[ny * w + nx]; + * } + * } + * } + * + * let alive = current[y * w + x]; + * let nextOutput = 0; + * if (alive == 1) { + * if (abs(n - 2) < 0.1 || abs(n - 3) < 0.1) { + * nextOutput = 1; + * } + * } else { + * if (abs(n - 3) < 0.1) { + * nextOutput = 1; + * } + * } + * next[y * w + x] = nextOutput; + * } + * + * function display() { + * let data = uniformStorage(() => cells); + * let w = uniformInt(() => W); + * let h = uniformInt(() => H); + * + * filterColor.begin(); + * let x = floor(filterColor.texCoord.x * w); + * let y = floor(filterColor.texCoord.y * h); + * let alive = data[y * w + x]; + * filterColor.set([alive, alive, alive, 1]); + * filterColor.end(); + * } + * + * function draw() { + * compute(gameShader, W, H); + * [nextCells, cells] = [cells, nextCells]; + * filter(displayShader); + * } + * ``` * * @method createStorage * @submodule p5.strands diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 8efdd64913..b4e155349d 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -344,7 +344,8 @@ if (typeof p5 !== "undefined") { * worldInputs.begin(); * // Spread spheres evenly across the canvas based on their index * let spacing = width / count; - * worldInputs.position.x += (instanceID() - (count - 1) / 2) * spacing; + * worldInputs.position.x += + * (instanceID() - (count - 1) / 2) * spacing; * worldInputs.end(); * } * @@ -374,8 +375,16 @@ if (typeof p5 !== "undefined") { * let data = []; * for (let i = 0; i < count; i++) { * data.push({ - * position: createVector((i - (count - 1) / 2) * 40, 0, 0), - * color: color(random(255), random(255), random(255)), + * position: createVector( + * random(-1, 1) * width / 2, + * random(-1, 1) * height / 2, + * 0, + * ), + * color: color( + * random(255), + * random(255), + * random(255) + * ) * }); * } * instanceData = createStorage(data); diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 23fe61b123..85cef0433e 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -969,7 +969,7 @@ function loading(p5, fn){ /** * Draws a p5.Geometry object to the canvas. * - * The parameter, `model`, is the + * The first parameter, `model`, is the * p5.Geometry object to draw. * p5.Geometry objects can be built with * buildGeometry(). They can also be loaded from @@ -977,11 +977,7 @@ function loading(p5, fn){ * * Note: `model()` can only be used in WebGL mode. * - * @method model - * @param {p5.Geometry} model 3D shape to be drawn. - * - * @param {Number} [count=1] number of instances to draw. - * @example + * ```js example * // Click and drag the mouse to view the scene from different angles. * * let shape; @@ -1009,8 +1005,9 @@ function loading(p5, fn){ * function createShape() { * cone(); * } + * ``` * - * @example + * ```js example * // Click and drag the mouse to view the scene from different angles. * * let shape; @@ -1056,8 +1053,9 @@ function loading(p5, fn){ * cylinder(3, 20); * pop(); * } + * ``` * - * @example + * ```js example * // Click and drag the mouse to view the scene from different angles. * * let shape; @@ -1079,6 +1077,51 @@ function loading(p5, fn){ * // Draw the shape. * model(shape); * } + * ``` + * + * Multiple instances can be drawn at once with `model(geometry, count)`. On its own, + * all the instances get drawn to the same spot, but you can use + * `instanceID()` inside of a shader to handle each instance. + * At large counts, this often runs faster than using a `for` loop. + * + * ```js example + * let instancesShader; + * let instance; + * let count = 5; + * + * function drawInstance() { + * sphere(15); + * } + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * instance = buildGeometry(drawInstance); + * instancesShader = buildMaterialShader(drawSpaced); + * } + * + * function drawSpaced() { + * worldInputs.begin(); + * // Spread spheres evenly across the canvas based on their index + * let spacing = width / count; + * worldInputs.position.x += + * (instanceID() - (count - 1) / 2) * spacing; + * worldInputs.end(); + * } + * + * function draw() { + * background(220); + * lights(); + * noStroke(); + * fill('red'); + * shader(instancesShader); + * model(instance, count); + * } + * ``` + * + * @method model + * @param {p5.Geometry} model 3D shape to be drawn. + * + * @param {Number} [count=1] number of instances to draw. */ fn.model = function (model, count = 1) { this._assert3d('model'); From 68700ca645019db992b892dd10381984b6be0140 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 25 Mar 2026 18:12:23 -0400 Subject: [PATCH 117/250] Document update() --- src/webgpu/p5.RendererWebGPU.js | 79 ++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index d71054111a..9bfad4e023 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -50,19 +50,16 @@ function rendererWebGPU(p5, fn) { } /** - * Updates the contents of the storage buffer with new data. - * - * The format of the new data must match the format used when the buffer - * was created with `createStorage()`: - * an array of objects for struct buffers, or an array/Float32Array of - * floats for plain float buffers. + * Updates the data in the buffer with new values. The new data must be in + * the same format as the data originally passed to + * `createStorage()`. * * @method update * @for p5.StorageBuffer * @beta * @webgpu * @webgpuOnly - * @param {Array|Float32Array} data The new data to write into the buffer. + * @param {Number[]|Float32Array|Object[]} data The new data to write into the buffer. */ update(data) { const device = this._renderer.device; @@ -120,12 +117,70 @@ function rendererWebGPU(p5, fn) { } /** - * Represents a GPU storage buffer created by createStorage(). + * A block of data that shaders can read from, and compute shaders can also + * write to. This is only available in WebGPU mode. + * + * Note: `createStorage()` is the recommended + * way to create an instance of this class. + * + * ```js example + * let particles; + * let computeShader; + * let displayShader; + * let instance; + * const numParticles = 100; + * + * async function setup() { + * await createCanvas(100, 100, WEBGPU); + * particles = createStorage(makeParticles(width / 2, height / 2)); + * computeShader = buildComputeShader(simulate); + * displayShader = buildMaterialShader(display); + * instance = buildGeometry(drawParticle); + * } + * + * function makeParticles(x, y) { + * let data = []; + * for (let i = 0; i < numParticles; i++) { + * let angle = (i / numParticles) * TWO_PI; + * let speed = random(0.5, 2); + * data.push({ + * position: createVector(x, y), + * velocity: createVector(cos(angle) * speed, sin(angle) * speed), + * }); + * } + * return data; + * } + * + * function drawParticle() { + * sphere(2); + * } + * + * function simulate() { + * let data = uniformStorage(particles); + * let idx = index.x; + * data[idx].position = data[idx].position + data[idx].velocity; + * } + * + * function display() { + * let data = uniformStorage(particles); + * worldInputs.begin(); + * let pos = data[instanceID()].position; + * worldInputs.position.xy += pos - [width / 2, height / 2]; + * worldInputs.end(); + * } * - * Storage buffers hold data that can be read and written by compute shaders, - * and read by vertex and fragment shaders. Pass a `p5.StorageBuffer` to - * setUniform() or - * uniformStorage() to bind it to a shader. + * function draw() { + * background(30); + * if (frameCount % 60 === 0) { + * particles.update(makeParticles(random(width), random(height))); + * } + * compute(computeShader, numParticles); + * noStroke(); + * fill(255, 200, 50); + * shader(displayShader); + * model(instance, numParticles); + * } + * ``` * * @class p5.StorageBuffer * @beta From 8cfbbda900ef179a0997a1671bbcc566de439052 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 25 Mar 2026 18:17:23 -0400 Subject: [PATCH 118/250] Add describe() calls --- src/core/p5.Renderer3D.js | 6 ++++++ src/strands/p5.strands.js | 2 ++ src/webgpu/p5.RendererWebGPU.js | 1 + 3 files changed, 9 insertions(+) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 6ca6719f6f..a3d312a8d6 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -2038,6 +2038,7 @@ function renderer3D(p5, fn) { * instanceData = createStorage(data); * instance = buildGeometry(drawInstance); * instancesShader = buildMaterialShader(drawInstances); + * describe('Five spheres at random positions, each a different random color.'); * } * * function drawInstance() { @@ -2092,6 +2093,7 @@ function renderer3D(p5, fn) { * * gameShader = buildComputeShader(simulate); * displayShader = buildFilterShader(display); + * describe('An animated Game of Life simulation displayed as black and white pixels.'); * } * * function simulate() { @@ -2231,6 +2233,7 @@ function renderer3D(p5, fn) { * computeShader = buildComputeShader(simulate); * displayShader = buildMaterialShader(display); * instance = buildGeometry(drawParticle); + * describe('100 orange particles shooting outward.'); * } * * function makeParticles(x, y) { @@ -2305,6 +2308,7 @@ function renderer3D(p5, fn) { * computeShader = buildComputeShader(simulate); * displayShader = buildMaterialShader(display); * instance = buildGeometry(drawParticle); + * describe('50 white spheres bouncing around the canvas.'); * } * * function drawParticle() { @@ -2406,6 +2410,7 @@ function renderer3D(p5, fn) { * computeShader = buildComputeShader(simulate); * displayShader = buildMaterialShader(display); * instance = buildGeometry(drawParticle); + * describe('50 white spheres bouncing around the canvas.'); * } * * function drawParticle() { @@ -2474,6 +2479,7 @@ function renderer3D(p5, fn) { * * gameShader = buildComputeShader(simulate); * displayShader = buildFilterShader(display); + * describe('An animated Game of Life simulation displayed as black and white pixels.'); * } * * function simulate() { diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index b4e155349d..d869850c5e 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -338,6 +338,7 @@ if (typeof p5 !== "undefined") { * createCanvas(200, 200, WEBGL); * instance = buildGeometry(drawInstance); * instancesShader = buildMaterialShader(drawSpaced); + * describe('Five red spheres arranged in a horizontal line.'); * } * * function drawSpaced() { @@ -390,6 +391,7 @@ if (typeof p5 !== "undefined") { * instanceData = createStorage(data); * instance = buildGeometry(drawInstance); * instancesShader = buildMaterialShader(drawInstances); + * describe('Five spheres at random positions, each a different random color.'); * } * * function drawInstance() { diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 9bfad4e023..52c4a60096 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -136,6 +136,7 @@ function rendererWebGPU(p5, fn) { * computeShader = buildComputeShader(simulate); * displayShader = buildMaterialShader(display); * instance = buildGeometry(drawParticle); + * describe('100 orange particles shooting outward.'); * } * * function makeParticles(x, y) { From b240418cd2d78f54df6ab5afb41ff2b9606fa986 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 25 Mar 2026 18:22:20 -0400 Subject: [PATCH 119/250] Move example to the right spot, add submodule for index --- src/core/p5.Renderer3D.js | 1 + src/webgpu/p5.RendererWebGPU.js | 120 ++++++++++++++++---------------- 2 files changed, 61 insertions(+), 60 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index a3d312a8d6..8ba065eebc 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -2569,6 +2569,7 @@ function renderer3D(p5, fn) { * `index.x` to get the index when looping in one dimension. * * @property index + * @submodule p5.strands * @beta * @webgpu * @webgpuOnly diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 52c4a60096..6b02a3be2a 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -54,6 +54,66 @@ function rendererWebGPU(p5, fn) { * the same format as the data originally passed to * `createStorage()`. * + * ```js example + * let particles; + * let computeShader; + * let displayShader; + * let instance; + * const numParticles = 100; + * + * async function setup() { + * await createCanvas(100, 100, WEBGPU); + * particles = createStorage(makeParticles(width / 2, height / 2)); + * computeShader = buildComputeShader(simulate); + * displayShader = buildMaterialShader(display); + * instance = buildGeometry(drawParticle); + * describe('100 orange particles shooting outward.'); + * } + * + * function makeParticles(x, y) { + * let data = []; + * for (let i = 0; i < numParticles; i++) { + * let angle = (i / numParticles) * TWO_PI; + * let speed = random(0.5, 2); + * data.push({ + * position: createVector(x, y), + * velocity: createVector(cos(angle) * speed, sin(angle) * speed), + * }); + * } + * return data; + * } + * + * function drawParticle() { + * sphere(2); + * } + * + * function simulate() { + * let data = uniformStorage(particles); + * let idx = index.x; + * data[idx].position = data[idx].position + data[idx].velocity; + * } + * + * function display() { + * let data = uniformStorage(particles); + * worldInputs.begin(); + * let pos = data[instanceID()].position; + * worldInputs.position.xy += pos - [width / 2, height / 2]; + * worldInputs.end(); + * } + * + * function draw() { + * background(30); + * if (frameCount % 60 === 0) { + * particles.update(makeParticles(random(width), random(height))); + * } + * compute(computeShader, numParticles); + * noStroke(); + * fill(255, 200, 50); + * shader(displayShader); + * model(instance, numParticles); + * } + * ``` + * * @method update * @for p5.StorageBuffer * @beta @@ -123,66 +183,6 @@ function rendererWebGPU(p5, fn) { * Note: `createStorage()` is the recommended * way to create an instance of this class. * - * ```js example - * let particles; - * let computeShader; - * let displayShader; - * let instance; - * const numParticles = 100; - * - * async function setup() { - * await createCanvas(100, 100, WEBGPU); - * particles = createStorage(makeParticles(width / 2, height / 2)); - * computeShader = buildComputeShader(simulate); - * displayShader = buildMaterialShader(display); - * instance = buildGeometry(drawParticle); - * describe('100 orange particles shooting outward.'); - * } - * - * function makeParticles(x, y) { - * let data = []; - * for (let i = 0; i < numParticles; i++) { - * let angle = (i / numParticles) * TWO_PI; - * let speed = random(0.5, 2); - * data.push({ - * position: createVector(x, y), - * velocity: createVector(cos(angle) * speed, sin(angle) * speed), - * }); - * } - * return data; - * } - * - * function drawParticle() { - * sphere(2); - * } - * - * function simulate() { - * let data = uniformStorage(particles); - * let idx = index.x; - * data[idx].position = data[idx].position + data[idx].velocity; - * } - * - * function display() { - * let data = uniformStorage(particles); - * worldInputs.begin(); - * let pos = data[instanceID()].position; - * worldInputs.position.xy += pos - [width / 2, height / 2]; - * worldInputs.end(); - * } - * - * function draw() { - * background(30); - * if (frameCount % 60 === 0) { - * particles.update(makeParticles(random(width), random(height))); - * } - * compute(computeShader, numParticles); - * noStroke(); - * fill(255, 200, 50); - * shader(displayShader); - * model(instance, numParticles); - * } - * ``` - * * @class p5.StorageBuffer * @beta * @webgpu From 1ea0a28210c6d07d0d50d6a1af6efeb98df23bc4 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 25 Mar 2026 19:31:34 -0400 Subject: [PATCH 120/250] Add more missing beta tags --- docs/parameterData.json | 572 +++++++++++++++++++------------------- src/strands/p5.strands.js | 3 + src/webgl/loading.js | 2 +- src/webgl/material.js | 1 + 4 files changed, 298 insertions(+), 280 deletions(-) diff --git a/docs/parameterData.json b/docs/parameterData.json index 14ccd90ad1..fe77a275eb 100644 --- a/docs/parameterData.json +++ b/docs/parameterData.json @@ -517,76 +517,6 @@ ] ] }, - "smoothstep": { - "overloads": [ - [ - "Number", - "Number", - "Number" - ] - ] - }, - "uniformStorage": { - "overloads": [ - [ - "String", - "p5.StorageBuffer|Function|Object?" - ], - [ - "p5.StorageBuffer|Function|Object?" - ] - ] - }, - "getTexture": { - "overloads": [ - [ - null, - null - ] - ] - }, - "getWorldInputs": { - "overloads": [ - [ - "Function" - ] - ] - }, - "getPixelInputs": { - "overloads": [ - [ - "Function" - ] - ] - }, - "getFinalColor": { - "overloads": [ - [ - "Function" - ] - ] - }, - "getColor": { - "overloads": [ - [ - "Function" - ] - ] - }, - "getObjectInputs": { - "overloads": [ - [ - "Function" - ] - ] - }, - "getCameraInputs": { - "overloads": [ - [ - "Function" - ] - ] - }, "bezier": { "overloads": [ [ @@ -690,6 +620,81 @@ [] ] }, + "instanceID": { + "overloads": [ + [] + ] + }, + "smoothstep": { + "overloads": [ + [ + "Number", + "Number", + "Number" + ] + ] + }, + "uniformStorage": { + "overloads": [ + [ + "String", + "p5.StorageBuffer|Function|Object?" + ], + [ + "p5.StorageBuffer|Function|Object?" + ] + ] + }, + "getTexture": { + "overloads": [ + [ + null, + null + ] + ] + }, + "getWorldInputs": { + "overloads": [ + [ + "Function" + ] + ] + }, + "getPixelInputs": { + "overloads": [ + [ + "Function" + ] + ] + }, + "getFinalColor": { + "overloads": [ + [ + "Function" + ] + ] + }, + "getColor": { + "overloads": [ + [ + "Function" + ] + ] + }, + "getObjectInputs": { + "overloads": [ + [ + "Function" + ] + ] + }, + "getCameraInputs": { + "overloads": [ + [ + "Function" + ] + ] + }, "nfc": { "overloads": [ [ @@ -735,15 +740,6 @@ ] ] }, - "resizeCanvas": { - "overloads": [ - [ - "Number", - "Number", - "Boolean?" - ] - ] - }, "push": { "overloads": [ [] @@ -776,6 +772,15 @@ ] ] }, + "resizeCanvas": { + "overloads": [ + [ + "Number", + "Number", + "Boolean?" + ] + ] + }, "month": { "overloads": [ [] @@ -831,11 +836,6 @@ ] ] }, - "noCanvas": { - "overloads": [ - [] - ] - }, "resetMatrix": { "overloads": [ [] @@ -855,6 +855,11 @@ ] ] }, + "noCanvas": { + "overloads": [ + [] + ] + }, "second": { "overloads": [ [] @@ -929,6 +934,13 @@ ] ] }, + "buildGeometry": { + "overloads": [ + [ + "Function" + ] + ] + }, "bezierPoint": { "overloads": [ [ @@ -940,13 +952,6 @@ ] ] }, - "buildGeometry": { - "overloads": [ - [ - "Function" - ] - ] - }, "loadModel": { "overloads": [ [ @@ -996,11 +1001,6 @@ [] ] }, - "clearStorage": { - "overloads": [ - [] - ] - }, "nfp": { "overloads": [ [ @@ -1015,6 +1015,11 @@ ] ] }, + "clearStorage": { + "overloads": [ + [] + ] + }, "removeElements": { "overloads": [ [] @@ -1117,6 +1122,13 @@ [] ] }, + "freeGeometry": { + "overloads": [ + [ + "p5.Geometry" + ] + ] + }, "cos": { "overloads": [ [ @@ -1171,13 +1183,6 @@ ] ] }, - "removeItem": { - "overloads": [ - [ - "String" - ] - ] - }, "rotate": { "overloads": [ [ @@ -1186,10 +1191,10 @@ ] ] }, - "freeGeometry": { + "removeItem": { "overloads": [ [ - "p5.Geometry" + "String" ] ] }, @@ -1339,6 +1344,16 @@ ] ] }, + "plane": { + "overloads": [ + [ + "Number?", + "Number?", + "Integer?", + "Integer?" + ] + ] + }, "red": { "overloads": [ [ @@ -1381,13 +1396,10 @@ ] ] }, - "plane": { + "strokeWeight": { "overloads": [ [ - "Number?", - "Number?", - "Integer?", - "Integer?" + "Number" ] ] }, @@ -1398,13 +1410,6 @@ ] ] }, - "strokeWeight": { - "overloads": [ - [ - "Number" - ] - ] - }, "degrees": { "overloads": [ [ @@ -1523,18 +1528,18 @@ ] ] }, - "clearDepth": { + "splitTokens": { "overloads": [ [ - "Number?" + "String", + "String?" ] ] }, - "splitTokens": { + "clearDepth": { "overloads": [ [ - "String", - "String?" + "Number?" ] ] }, @@ -1552,6 +1557,17 @@ ] ] }, + "box": { + "overloads": [ + [ + "Number?", + "Number?", + "Number?", + "Integer?", + "Integer?" + ] + ] + }, "max": { "overloads": [ [ @@ -1646,17 +1662,6 @@ [] ] }, - "box": { - "overloads": [ - [ - "Number?", - "Number?", - "Number?", - "Integer?", - "Integer?" - ] - ] - }, "char": { "overloads": [ [ @@ -1823,18 +1828,18 @@ [] ] }, - "loadPixels": { + "sphere": { "overloads": [ - [] + [ + "Number?", + "Integer?", + "Integer?" + ] ] }, - "loadBytes": { + "loadPixels": { "overloads": [ - [ - "String|Request", - "Function?", - "Function?" - ] + [] ] }, "buildFilterShader": { @@ -1849,6 +1854,15 @@ ] ] }, + "loadBytes": { + "overloads": [ + [ + "String|Request", + "Function?", + "Function?" + ] + ] + }, "createSlider": { "overloads": [ [ @@ -1868,15 +1882,6 @@ ] ] }, - "sphere": { - "overloads": [ - [ - "Number?", - "Integer?", - "Integer?" - ] - ] - }, "keyReleased": { "overloads": [ [ @@ -2103,7 +2108,19 @@ "hue": { "overloads": [ [ - "p5.Color|Number[]|String" + "p5.Color|Number[]|String" + ] + ] + }, + "cylinder": { + "overloads": [ + [ + "Number?", + "Number?", + "Integer?", + "Integer?", + "Boolean?", + "Boolean?" ] ] }, @@ -2190,18 +2207,6 @@ ] ] }, - "cylinder": { - "overloads": [ - [ - "Number?", - "Number?", - "Integer?", - "Integer?", - "Boolean?", - "Boolean?" - ] - ] - }, "getURL": { "overloads": [ [] @@ -2309,31 +2314,31 @@ ] ] }, - "mouseMoved": { + "shader": { "overloads": [ [ - "MouseEvent?" + "p5.Shader" ] ] }, - "mouseDragged": { + "mouseMoved": { "overloads": [ [ "MouseEvent?" ] ] }, - "keyIsDown": { + "mouseDragged": { "overloads": [ [ - "Number|String" + "MouseEvent?" ] ] }, - "shader": { + "keyIsDown": { "overloads": [ [ - "p5.Shader" + "Number|String" ] ] }, @@ -2344,14 +2349,6 @@ ] ] }, - "model": { - "overloads": [ - [ - "p5.Geometry", - "Number?" - ] - ] - }, "rect": { "overloads": [ [ @@ -2405,22 +2402,21 @@ ] ] }, - "createSelect": { + "model": { "overloads": [ [ - "Boolean?" - ], - [ - "Object" + "p5.Geometry", + "Number?" ] ] }, - "worldToScreen": { + "createSelect": { "overloads": [ [ - "Number|p5.Vector", - "Number", - "Number?" + "Boolean?" + ], + [ + "Object" ] ] }, @@ -2435,25 +2431,12 @@ ] ] }, - "createModel": { + "worldToScreen": { "overloads": [ [ - "String", - "String?", - "Boolean?", - "function(p5.Geometry)?", - "function(Event)?" - ], - [ - "String", - "String?", - "function(p5.Geometry)?", - "function(Event)?" - ], - [ - "String", - "String?", - "Object?" + "Number|p5.Vector", + "Number", + "Number?" ] ] }, @@ -2487,6 +2470,28 @@ ] ] }, + "createModel": { + "overloads": [ + [ + "String", + "String?", + "Boolean?", + "function(p5.Geometry)?", + "function(Event)?" + ], + [ + "String", + "String?", + "function(p5.Geometry)?", + "function(Event)?" + ], + [ + "String", + "String?", + "Object?" + ] + ] + }, "mousePressed": { "overloads": [ [ @@ -2856,17 +2861,17 @@ ] ] }, - "imageMode": { + "imageShader": { "overloads": [ [ - "CORNER|CORNERS|CENTER" + "p5.Shader" ] ] }, - "imageShader": { + "imageMode": { "overloads": [ [ - "p5.Shader" + "CORNER|CORNERS|CENTER" ] ] }, @@ -2941,14 +2946,6 @@ ] ] }, - "paletteLerp": { - "overloads": [ - [ - "[p5.Color|String|Number|Number[], Number][]", - "Number" - ] - ] - }, "torus": { "overloads": [ [ @@ -2959,6 +2956,14 @@ ] ] }, + "paletteLerp": { + "overloads": [ + [ + "[p5.Color|String|Number|Number[], Number][]", + "Number" + ] + ] + }, "close": { "overloads": [ [] @@ -3022,21 +3027,21 @@ ] ] }, - "save": { + "loadMaterialShader": { "overloads": [ [ - "Object|String?", - "String?", - "Boolean|String?" + "String", + "Function?", + "Function?" ] ] }, - "loadMaterialShader": { + "save": { "overloads": [ [ - "String", - "Function?", - "Function?" + "Object|String?", + "String?", + "Boolean|String?" ] ] }, @@ -3060,6 +3065,11 @@ [] ] }, + "baseFilterShader": { + "overloads": [ + [] + ] + }, "erase": { "overloads": [ [ @@ -3068,11 +3078,6 @@ ] ] }, - "baseFilterShader": { - "overloads": [ - [] - ] - }, "noErase": { "overloads": [ [] @@ -3258,6 +3263,18 @@ ] ] }, + "createStorage": { + "overloads": [ + [ + "Number|Array|Float32Array|Object[]" + ] + ] + }, + "baseComputeShader": { + "overloads": [ + [] + ] + }, "buildStrokeShader": { "overloads": [ [ @@ -3281,22 +3298,22 @@ ] ] }, - "perspective": { + "loadStrokeShader": { "overloads": [ [ - "Number?", - "Number?", - "Number?", - "Number?" + "String", + "Function?", + "Function?" ] ] }, - "loadStrokeShader": { + "perspective": { "overloads": [ [ - "String", - "Function?", - "Function?" + "Number?", + "Number?", + "Number?", + "Number?" ] ] }, @@ -3312,12 +3329,11 @@ ] ] }, - "linePerspective": { + "buildComputeShader": { "overloads": [ [ - "Boolean" - ], - [] + "Function" + ] ] }, "resetShader": { @@ -3325,6 +3341,14 @@ [] ] }, + "linePerspective": { + "overloads": [ + [ + "Boolean" + ], + [] + ] + }, "ortho": { "overloads": [ [ @@ -3344,6 +3368,16 @@ ] ] }, + "compute": { + "overloads": [ + [ + "p5.Shader", + "Number", + "Number?", + "Number?" + ] + ] + }, "vertex": { "overloads": [ [ @@ -3365,6 +3399,13 @@ ] ] }, + "curveDetail": { + "overloads": [ + [ + "Number" + ] + ] + }, "frustum": { "overloads": [ [ @@ -3377,13 +3418,6 @@ ] ] }, - "curveDetail": { - "overloads": [ - [ - "Number" - ] - ] - }, "createCamera": { "overloads": [ [] @@ -3483,41 +3517,12 @@ ] ] }, - "createStorage": { - "overloads": [ - [ - "Number|Array|Float32Array|Object[]" - ] - ] - }, - "baseComputeShader": { - "overloads": [ - [] - ] - }, "metalness": { "overloads": [ [ "Number" ] ] - }, - "buildComputeShader": { - "overloads": [ - [ - "Function" - ] - ] - }, - "compute": { - "overloads": [ - [ - "p5.Shader", - "Number", - "Number?", - "Number?" - ] - ] } }, "p5.Element": { @@ -4440,7 +4445,7 @@ "overloads": [ [ "String", - "Boolean|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture|p5.StorageBuffer" + "Boolean|p5.Vector|p5.Color|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture|p5.StorageBuffer" ] ] } @@ -4557,6 +4562,15 @@ ] } }, + "p5.StorageBuffer": { + "update": { + "overloads": [ + [ + "Number[]|Float32Array|Object[]" + ] + ] + } + }, "p5.Geometry": { "calculateBoundingBox": { "overloads": [ diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index d869850c5e..d8c839847e 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -764,6 +764,7 @@ if (typeof p5 !== "undefined") { /** * @property {Object} filterColor + * @beta * @description * A shader hook block that sets the color for each pixel in a filter shader. This hook can be used inside `buildFilterShader()` to control the output color for each pixel. * @@ -808,6 +809,7 @@ if (typeof p5 !== "undefined") { /** * @property {Object} objectInputs + * @beta * @description * A shader hook block to modify the properties of each vertex before any transformations are applied. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to customize vertex positions, normals, texture coordinates, and colors before rendering. Modifications happen between the `.begin()` and `.end()` methods of the hook. "Object space" refers to the coordinate system of the 3D scene before any transformations, cameras, or projection transformations are applied. * @@ -849,6 +851,7 @@ if (typeof p5 !== "undefined") { /** * @property {Object} cameraInputs + * @beta * @description * A shader hook block that adjusts vertex properties from the perspective of the camera. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to customize vertex positions, normals, texture coordinates, and colors before rendering. "Camera space" refers to the coordinate system of the 3D scene after transformations have been applied, seen relative to the camera. * diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 85cef0433e..e1d726389c 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -1081,7 +1081,7 @@ function loading(p5, fn){ * * Multiple instances can be drawn at once with `model(geometry, count)`. On its own, * all the instances get drawn to the same spot, but you can use - * `instanceID()` inside of a shader to handle each instance. + * `instanceID()` inside of a shader to handle each instance. * At large counts, this often runs faster than using a `for` loop. * * ```js example diff --git a/src/webgl/material.js b/src/webgl/material.js index 2e1bc0c7e1..e1551c2f29 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -500,6 +500,7 @@ function material(p5, fn) { * `loadFilterShader('myShader.js', onLoaded)`. * * @method loadFilterShader + * @beta * @submodule p5.strands * @param {String} filename path to a p5.strands JavaScript file or a GLSL fragment shader file * @param {Function} [successCallback] callback to be called once the shader is From 4704868832f425aa100b0769e8b5e4c7cd06e329 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 25 Mar 2026 19:32:38 -0400 Subject: [PATCH 121/250] baseComputeShader docs updates --- src/core/p5.Renderer3D.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 8ba065eebc..2da3b3efad 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -2170,10 +2170,14 @@ function renderer3D(p5, fn) { }; /** - * Returns the base compute shader. + * Returns the default shader used for compute operations. * - * Calling `buildComputeShader(shaderFunction)` is equivalent to - * calling `baseComputeShader().modify(shaderFunction)`. + * Calling `buildComputeShader(shaderFunction)` + * is equivalent to calling `baseComputeShader().modify(shaderFunction)`. + * + * Read the `buildComputeShader` reference or + * call `baseComputeShader().inspectHooks()` for more information on what you can do with + * the base compute shader. * * @method baseComputeShader * @submodule p5.strands From 0bf343a3cea8f906cf11474d6fb4e8805d347ce6 Mon Sep 17 00:00:00 2001 From: dhowe Date: Thu, 26 Mar 2026 18:06:59 +0000 Subject: [PATCH 122/250] fix to tint in Renderer3D --- src/core/p5.Renderer3D.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 5f29e7a60c..b98cb19178 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -1486,7 +1486,7 @@ export class Renderer3D extends Renderer { // works differently and is global p5 state. If the p5 state has // been cleared, we also need to clear the value in uSampler to match. fillShader.setUniform("uSampler", this.states._tex || empty); - fillShader.setUniform("uTint", this.states.tint.array().map(v => v * 255)); + fillShader.setUniform("uTint", this.states.tint._getRGBA([255, 255, 255, 255])); fillShader.setUniform("uHasSetAmbient", this.states._hasSetAmbient); fillShader.setUniform("uAmbientMatColor", this.states.curAmbientColor); From 7d9298c63fbc2ab373d0df8736cb49f3cab0a380 Mon Sep 17 00:00:00 2001 From: VANSH3104 Date: Fri, 27 Mar 2026 16:42:39 +0530 Subject: [PATCH 123/250] Fix: Initialize shape with vertex properties in arc() to match ellipse() pattern --- src/shape/custom_shapes.js | 90 ++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 37 deletions(-) diff --git a/src/shape/custom_shapes.js b/src/shape/custom_shapes.js index a10fddf885..30862879ab 100644 --- a/src/shape/custom_shapes.js +++ b/src/shape/custom_shapes.js @@ -495,6 +495,8 @@ class ArcPrimitive extends ShapePrimitive { get start() { return this.#start; } get stop() { return this.#stop; } get mode() { return this.#mode; } + get startVertex() { return this.vertices[0]; } + get endVertex() { return this.vertices[1]; } get vertexCapacity() { return this.#vertexCapacity; @@ -976,20 +978,23 @@ class Shape { arcPrimitive(x,y,w,h,start,stop,mode){ + this.beginShape(); const centerX = x+w/2; const centerY = y+h/2; + const radiusX = w / 2; + const radiusY = h / 2; const startVertex = this.#createVertex( new Vector( - centerX+(w/2)*Math.cos(start), - centerY+(h/2)*Math.sin(start) + centerX + radiusX * Math.cos(start), + centerY + radiusY * Math.sin(start) ) ); const endVertex = this.#createVertex( new Vector( - centerX+(w/2)*Math.cos(stop), - centerY+(h/2)*Math.sin(stop) + centerX + radiusX * Math.cos(stop), + centerY + radiusY * Math.sin(stop) ) ); @@ -1001,7 +1006,9 @@ class Shape { stop, mode ); - return primitive.addToShape(this); + primitive.addToShape(this); + this.endShape(); + return this; } @@ -1419,44 +1426,47 @@ class PrimitiveToVerticesConverter extends PrimitiveVisitor { this.contours.push(quadStrip.vertices.slice()); } visitArcPrimitive(arc) { + const startVertex = arc.startVertex; + const endVertex = arc.endVertex; const centerX = arc.x + arc.w / 2; const centerY = arc.y + arc.h / 2; const radiusX = arc.w / 2; const radiusY = arc.h / 2; - const avgRadius = (radiusX+radiusY)/2; + const avgRadius = (radiusX + radiusY) / 2; - const arcLength = avgRadius*Math.abs(arc.stop-arc.start); + const arcLength = avgRadius * Math.abs(arc.stop - arc.start); - const numPoints=Math.max(3, Math.ceil(this.curveDetail*arcLength)); + const numPoints = Math.max(3, Math.ceil(this.curveDetail * arcLength)); const verts = []; - + const interpolateVertexProps = (v1, v2, t) => { + const props = {}; + for (const [key, value] of Object.entries(v1)) { + if (key === 'position') continue; + if (typeof value === 'number' && typeof v2[key] === 'number') { + props[key] = value * (1 - t) + v2[key] * t; + } else { + props[key] = value; + } + } + return props; + }; if (arc.mode === constants.PIE) { - verts.push(new Vertex({ position: new Vector(centerX, centerY) })); + const centerProps = interpolateVertexProps(startVertex, endVertex, 0.5); + centerProps.position = new Vector(centerX, centerY); + verts.push(new Vertex(centerProps)); } for (let i = 0; i <= numPoints; i++) { - const angle = arc.start + (arc.stop - arc.start) * (i / numPoints); - const startVertex=arc.vertices[0]; - const endVertex=arc.vertices[1]; - const t=i/numPoints; - const props={}; - for(const key in startVertex){ - if(key === 'position') continue; - if(typeof startVertex[key] === 'number' - && typeof endVertex[key]=== 'number'){ - props[key] = startVertex[key]*(1-t) + endVertex[key]*t; - } - else{ - props[key]=startVertex[key]; - } - } + const t = i / numPoints; + const angle = arc.start + (arc.stop - arc.start) * t; + const vertexProps = interpolateVertexProps(startVertex, endVertex, t); - props.position=new Vector( - centerX+radiusX*Math.cos(angle), - centerY+radiusY*Math.sin(angle) + vertexProps.position = new Vector( + centerX + radiusX * Math.cos(angle), + centerY + radiusY * Math.sin(angle) ); - verts.push(new Vertex(props)); + verts.push(new Vertex(vertexProps)); } this.contours.push(verts); @@ -1466,17 +1476,23 @@ class PrimitiveToVerticesConverter extends PrimitiveVisitor { const centerY = ellipse.y + ellipse.h / 2; const radiusX = ellipse.w / 2; const radiusY = ellipse.h / 2; - const numPoints = Math.max(3, this.curveDetail); + const avgRadius = (radiusX + radiusY) / 2; + const perimeter = 2 * Math.PI * avgRadius; + const numPoints = Math.max(3, Math.ceil(this.curveDetail * perimeter)); const verts = []; - + const centerVertex = ellipse.vertices[0]; for (let i = 0; i <= numPoints; i++) { const angle = (2 * Math.PI * i) / numPoints; - verts.push(new Vertex({ - position: new Vector( - centerX + radiusX * Math.cos(angle), - centerY + radiusY * Math.sin(angle) - ) - })); + const vertexProps = {}; + for (const [key, value] of Object.entries(centerVertex)) { + if (key === 'position') continue; + vertexProps[key] = value; + } + vertexProps.position = new Vector( + centerX + radiusX * Math.cos(angle), + centerY + radiusY * Math.sin(angle) + ); + verts.push(new Vertex(vertexProps)); } this.contours.push(verts); From f2b4c2eb91296d9b0fa057691ee3687badb3952d Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sat, 28 Mar 2026 17:35:56 +0000 Subject: [PATCH 124/250] Example typo fix --- src/color/setting.js | 2 +- src/webgl/p5.Geometry.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/color/setting.js b/src/color/setting.js index cae961600b..a8e8d0b1b3 100644 --- a/src/color/setting.js +++ b/src/color/setting.js @@ -509,7 +509,7 @@ function setting(p5, fn){ * * describe('A canvas with a transparent green background.'); * } - * < + * * @example * function setup() { * createCanvas(100, 100); diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index dad4c860f4..a2c8992885 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -330,7 +330,7 @@ class Geometry { * let saveBtn; * function setup() { * createCanvas(200, 200, WEBGL); - * myModel = buildGeometry(function()) { + * myModel = buildGeometry(function() { * for (let i = 0; i < 5; i++) { * push(); * translate( From f8fa8089384a38542b9a161c4ee77eb8bded60db Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Mon, 30 Mar 2026 02:26:29 +0530 Subject: [PATCH 125/250] Fix: restore lerp alias to GLSL mix in p5.strands --- src/strands/strands_api.js | 10 +++++ test/unit/visual/cases/webgl.js | 40 ++++++++++++++++++ .../000.png | Bin 0 -> 236 bytes .../metadata.json | 3 ++ .../000.png | Bin 0 -> 236 bytes .../metadata.json | 3 ++ 6 files changed, 56 insertions(+) create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/lerp maps to mix in strands context/000.png create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/lerp maps to mix in strands context/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/mix produces same result as lerp in strands/000.png create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/mix produces same result as lerp in strands/metadata.json diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index f08aa3bc15..d145cf7298 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -284,6 +284,16 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } } + // Alias lerp to GLSL mix in strands context + const originalLerp = fn.lerp; + augmentFn(fn, p5, 'lerp', function (...args) { + if (strandsContext.active) { + return fn.mix(...args); + } else { + return originalLerp.apply(this, args); + } + }); + augmentFn(fn, p5, 'getTexture', function (...rawArgs) { if (strandsContext.active) { const { id, dimension } = strandsContext.backend.createGetTextureCall(strandsContext, rawArgs); diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 1893e40252..b83b9748b7 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -1067,6 +1067,46 @@ visualSuite('WebGL', function() { screenshot(); }); + visualTest('lerp maps to mix in strands context', (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + // lerp should behave identically to mix inside strands + const shader = p5.baseColorShader().modify(() => { + p5.getFinalColor((color) => { + color = p5.lerp( + [1, 0, 0, 1], + [0, 0, 1, 1], + 0.5 + ); + return color; + }); + }, { p5 }); + p5.background(0); + p5.shader(shader); + p5.noStroke(); + p5.plane(50, 50); + screenshot(); + }); + + visualTest('mix produces same result as lerp in strands', (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + // mix directly, should produce identical output to lerp test above + const shader = p5.baseColorShader().modify(() => { + p5.getFinalColor((color) => { + color = p5.mix( + [1, 0, 0, 1], + [0, 0, 1, 1], + 0.5 + ); + return color; + }); + }, { p5 }); + p5.background(0); + p5.shader(shader); + p5.noStroke(); + p5.plane(50, 50); + screenshot(); + }); + visualSuite('auto-return for shader hooks', () => { visualTest('auto-returns input struct when return is omitted', (p5, screenshot) => { p5.createCanvas(50, 50, p5.WEBGL); diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/lerp maps to mix in strands context/000.png b/test/unit/visual/screenshots/WebGL/p5.strands/lerp maps to mix in strands context/000.png new file mode 100644 index 0000000000000000000000000000000000000000..223241d6cd81709cee64286a1819bafc51fe3d28 GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nETg`O^sAr*{oCOL977zi+X$KRX( zn!V^|LdO)rJuGGiCBH{L-}&(F>q+hWKWBUNB&fJF#A~x$TX(dhSFfaFqCmuMF=ew| z7Zlx`65dAj+<4*CE+Lq(TF`CPkE0fDEK{;tCTi6g_6agBl}cRF72lKW;;_<9V&x<8 z#}a}8tKiD?7Z-}V9XoM|U4oNY?s{@ck4{a)M1u!~e@)%ufNuQ%zi-X+V?YKQ$g#`} YvkX&=|Cej+0g5nqy85}Sb4q9e02)wM-T(jq literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/lerp maps to mix in strands context/metadata.json b/test/unit/visual/screenshots/WebGL/p5.strands/lerp maps to mix in strands context/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/p5.strands/lerp maps to mix in strands context/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/mix produces same result as lerp in strands/000.png b/test/unit/visual/screenshots/WebGL/p5.strands/mix produces same result as lerp in strands/000.png new file mode 100644 index 0000000000000000000000000000000000000000..223241d6cd81709cee64286a1819bafc51fe3d28 GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nETg`O^sAr*{oCOL977zi+X$KRX( zn!V^|LdO)rJuGGiCBH{L-}&(F>q+hWKWBUNB&fJF#A~x$TX(dhSFfaFqCmuMF=ew| z7Zlx`65dAj+<4*CE+Lq(TF`CPkE0fDEK{;tCTi6g_6agBl}cRF72lKW;;_<9V&x<8 z#}a}8tKiD?7Z-}V9XoM|U4oNY?s{@ck4{a)M1u!~e@)%ufNuQ%zi-X+V?YKQ$g#`} YvkX&=|Cej+0g5nqy85}Sb4q9e02)wM-T(jq literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/mix produces same result as lerp in strands/metadata.json b/test/unit/visual/screenshots/WebGL/p5.strands/mix produces same result as lerp in strands/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/p5.strands/mix produces same result as lerp in strands/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file From 458ce4d35ebe0d02be4c50ff9781812715a141c0 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 30 Mar 2026 19:33:51 -0400 Subject: [PATCH 126/250] Add comments about duck typing usage --- src/color/p5.Color.js | 6 +++++- src/math/p5.Vector.js | 6 ++++++ src/webgl/p5.Shader.js | 4 ++++ src/webgpu/p5.RendererWebGPU.js | 7 +++++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/color/p5.Color.js b/src/color/p5.Color.js index 399aa2c4ee..e3724bf4ff 100644 --- a/src/color/p5.Color.js +++ b/src/color/p5.Color.js @@ -61,7 +61,11 @@ class Color { static #colorjsMaxes = {}; static #grayscaleMap = {}; - // For duck typing + // This property is here where duck typing (checking if obj.isColor) needs + // to be used over more standard type checking (obj instanceof Color). This + // needs to happen where we are building multiple files, such as in p5.webgpu.js, + // where if we `import { Color }` directly, it will be a separate copy of the + // Color class from the one imported in the main p5.js bundle. isColor = true; // Used to add additional color modes to p5.js diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 8cd8334072..fa3455c707 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -48,6 +48,12 @@ class Vector { this.dimensions = dimensions; this._values = values; } + + // This property is here where duck typing (checking if obj.isVector) needs + // to be used over more standard type checking (obj instanceof Vector). This + // needs to happen where we are building multiple files, such as in p5.webgpu.js, + // where if we `import { Vector }` directly, it will be a separate copy of the + // Vector class from the one imported in the main p5.js bundle. this.isVector = true; } diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index a16f6b8907..4fb349a9ff 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -1099,6 +1099,10 @@ class Shader { return; } + // In p5.strands-related code, where some of the code may be in + // p5.webgpu.js instead of the main p5.js build, we generally use + // duck typing instead of instanceof to avoid accidentally importing + // and comparing against a separate copy of p5 classes if (data?.isVector) { data = data.values.length !== data.dimensions ? data.values.slice(0, data.dimensions) : data.values; } else if (data?.isColor) { diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 6b02a3be2a..895eb6d2b2 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1869,6 +1869,9 @@ function rendererWebGPU(p5, fn) { // value: number or number[] - the data to write _packField(field, value, floatView, dataView, baseOffset) { if (value === undefined) return; + + // Duck typing instead of instanceof to avoid importing a separate + // copy of the Color/Vector classes if (value?.isVector) { value = value.values.length !== value.dimensions ? value.values.slice(0, value.dimensions) : value.values; } else if (value?.isColor) { @@ -3113,6 +3116,8 @@ ${hookUniformFields}} // Maps a plain JS value to the WGSL type string that represents it in a struct. _jsValueToWgslType(value) { if (typeof value === 'number') return 'f32'; + // Duck typing instead of instanceof to avoid importing a separate + // copy of the Color/Vector classes if (value?.isVector) { if (value.dimensions === 2) return 'vec2f'; if (value.dimensions === 3) return 'vec3f'; @@ -3148,6 +3153,8 @@ ${hookUniformFields}} value !== null && typeof value === 'object' && !Array.isArray(value) && + // Duck typing instead of instanceof to avoid importing a separate + // copy of the Color/Vector classes !value?.isVector && !value?.isColor ) { From 85e5127ecaf6673505f56e2d8c9b9577b784a334 Mon Sep 17 00:00:00 2001 From: kit <1304340+ksen0@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:57:02 +0200 Subject: [PATCH 127/250] Revise p5.js 2.0 Beta Bug Report template Updated the p5.js 2.0 Beta Bug Report template to reflect changes in behavior and proposal considerations. --- .../ISSUE_TEMPLATE/1-p5.js-2.0-bug-report.yml | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/1-p5.js-2.0-bug-report.yml b/.github/ISSUE_TEMPLATE/1-p5.js-2.0-bug-report.yml index bb04352fa9..b68ef85bfc 100644 --- a/.github/ISSUE_TEMPLATE/1-p5.js-2.0-bug-report.yml +++ b/.github/ISSUE_TEMPLATE/1-p5.js-2.0-bug-report.yml @@ -1,18 +1,16 @@ -name: 📃 p5.js 2.0 Beta Bug Report -description: This template is for submitting a bug report for bugs found in the p5.js 2.0 beta releases. -title: "[p5.js 2.0 Beta Bug Report]: " -labels: [p5.js 2.0] +name: 📃 p5.js 2.0+ Bug Report +description: This template is for submitting a bug report for bugs found in p5v2! +title: "[p5.js 2.0+ Bug Report]: " +labels: [p5.js 2.0+] body: - type: markdown attributes: value: | ### What falls under this category? - There has been many changes to p5.js in 2.0 that is currently released as beta versions. If you suspect there may be a bug, please follow the below steps before opening a bug report using this template: - - 1. There are some differences in behavior between p5.js 1.x and 2.0 beta, please check the changelog and/or [proposal list](https://github.com/orgs/processing/projects/21) to see if the difference in behavior is intended. If in doubt, feel free to open the issue anyway and ask. - 2. Breaking changes may still happen between beta versions, please make sure to include the full beta version number and use the latest beta release where possible. - 3. We are not considering any new proposal for p5.js 2.0 at this stage and if you would like to request new features, please use the "New feature request" issue template. - 4. The documentation and examples may be outdated at this stage while we work on updating them. + There has been many changes to p5.js in 2.0! You can try it in p5.js Editor by updating the version in "Settings," and reference is available [on the beta version of the site](https://beta.p5js.org/). If you suspect there may be a bug, please follow the below steps before opening a bug report using this template: + 1. There are some differences in behavior between p5.js 1.x and 2.0 beta, please check the changelog and/or [2.0 status board](https://github.com/orgs/processing/projects/21) to see if the difference in behavior is intended. If in doubt, feel free to open the issue anyway and ask. + 2. New proposals for future p5.js 2.x minor releases may be considered! Please use the "New feature request" issue template, or check the [2.0 status board](https://github.com/orgs/processing/projects/21) is there is already discussion abotu this proposal. + 3. Reports of errors or potential improvements in documentation and examples is expecially helpful, since many things have been updated from 1.x to 2.0! - type: checkboxes id: sub-area attributes: @@ -31,6 +29,8 @@ body: - label: Typography - label: Utilities - label: WebGL + - label: WebGPU + - label: p5.strands - label: Build process - label: Unit testing - label: Internationalization @@ -78,4 +78,4 @@ body: ```" validations: - required: true \ No newline at end of file + required: true From 527648fc4798e27bb72790d03a9c7aafd4ec25a1 Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Wed, 1 Apr 2026 21:23:57 +0530 Subject: [PATCH 128/250] Make instanceID() work in both vertex and fragment shaders --- src/strands/ir_types.js | 1 + src/strands/strands_codegen.js | 5 ++ src/webgl/p5.Shader.js | 5 ++ src/webgl/strands_glslBackend.js | 13 ++++- src/webgl/utils.js | 15 ++++++ src/webgpu/p5.RendererWebGPU.js | 46 +++++++++++++++++- src/webgpu/strands_wgslBackend.js | 12 ++++- test/unit/visual/cases/webgl.js | 27 ++++++++++ .../000.png | Bin 0 -> 653 bytes .../metadata.json | 3 ++ 10 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGL/instanced randering/instanceID in fragment hook colors instances/000.png create mode 100644 test/unit/visual/screenshots/WebGL/instanced randering/instanceID in fragment hook colors instances/metadata.json diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index ff891106c6..8bede07242 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -11,6 +11,7 @@ export const NodeType = { STATEMENT: 'statement', ASSIGNMENT: 'assignment', }; +export const INSTANCE_ID_VARYING_NAME = '_p5_instanceID'; export const NodeTypeToName = Object.fromEntries( Object.entries(NodeType).map(([key, val]) => [val, key]) ); diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 89e1aed22a..bf1ea61b4d 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -96,6 +96,11 @@ export function generateShaderCode(strandsContext) { } } + // Register instanceID varying if used in a fragment hook + if (strandsContext._instanceIDUsedInFragment) { + hooksObj.instanceIDVarying = backend.generateInstanceIDVarying(); + } + hooksObj.vertexDeclarations = [...vertexDeclarations].join('\n'); hooksObj.fragmentDeclarations = [...fragmentDeclarations].join('\n'); hooksObj.computeDeclarations = [...computeDeclarations].join('\n'); diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 4fb349a9ff..a881825948 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -58,6 +58,9 @@ class Shader { // Stores an array of variable names + types passed between the vertex and fragment shader varyingVariables: options.varyingVariables || [], + // Stores instanceID varying info for forwarding to the fragment shader + instanceIDVarying: options.instanceIDVarying || null, + // Stores helper functions to prepend to shaders. helpers: options.helpers || {}, @@ -422,6 +425,7 @@ class Shader { if (key === 'uniforms') continue; if (key === 'storageUniforms') continue; if (key === 'varyingVariables') continue; + if (key === 'instanceIDVarying') continue; if (key === 'vertexDeclarations') { newHooks.vertex.declarations = (newHooks.vertex.declarations || '') + '\n' + hooks[key]; @@ -469,6 +473,7 @@ class Shader { uniforms: Object.assign({}, this.hooks.uniforms, hooks.uniforms || {}), storageUniforms: Object.assign({}, this.hooks.storageUniforms, hooks.storageUniforms || {}), varyingVariables: (hooks.varyingVariables || []).concat(this.hooks.varyingVariables || []), + instanceIDVarying: hooks.instanceIDVarying || this.hooks.instanceIDVarying || null, fragment: Object.assign({}, this.hooks.fragment, newHooks.fragment || {}), vertex: Object.assign({}, this.hooks.vertex, newHooks.vertex || {}), compute: Object.assign({}, this.hooks.compute, newHooks.compute || {}), diff --git a/src/webgl/strands_glslBackend.js b/src/webgl/strands_glslBackend.js index abc24566ad..d850aef91d 100644 --- a/src/webgl/strands_glslBackend.js +++ b/src/webgl/strands_glslBackend.js @@ -1,4 +1,4 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType, DataType } from "../strands/ir_types"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType, DataType, INSTANCE_ID_VARYING_NAME } from "../strands/ir_types"; import { getNodeDataFromID, extractNodeTypeInfo } from "../strands/ir_dag"; import * as FES from '../strands/strands_FES'; import * as build from '../strands/ir_builders'; @@ -274,6 +274,13 @@ export const glslBackend = { sharedVar.usedInFragment = true; } } + + // Detect instanceID usage in fragment context and rewrite to varying name + if (node.identifier === this.instanceIdReference() && generationContext.shaderContext === 'fragment') { + generationContext.strandsContext._instanceIDUsedInFragment = true; + return INSTANCE_ID_VARYING_NAME; + } + return node.identifier; case NodeType.OPERATION: const useParantheses = node.usedBy.length > 0; @@ -398,4 +405,8 @@ export const glslBackend = { instanceIdReference() { return 'gl_InstanceID'; }, + + generateInstanceIDVarying() { + return { name: INSTANCE_ID_VARYING_NAME, declaration: `int ${INSTANCE_ID_VARYING_NAME}`, source: 'gl_InstanceID', interpolation: 'flat' }; + }, } diff --git a/src/webgl/utils.js b/src/webgl/utils.js index c5c6e3d7c9..6944df60da 100644 --- a/src/webgl/utils.js +++ b/src/webgl/utils.js @@ -1,4 +1,5 @@ import * as constants from "../core/constants"; +import { INSTANCE_ID_VARYING_NAME } from "../strands/ir_types"; import { Texture } from "./p5.Texture"; /** @@ -429,6 +430,20 @@ export function populateGLSLHooks(shader, src, shaderType) { } } } + + // Handle instanceID varying for fragment access + if (shader.hooks.instanceIDVarying) { + const { declaration, source, interpolation } = shader.hooks.instanceIDVarying; + const qualifier = interpolation ? `${interpolation} ` : ''; + if (shaderType === "vertex") { + // Emit flat out declaration and inject assignment into main() body + hooks += `${qualifier}OUT ${declaration};\n`; + postMain = postMain.replace(/\{/, `{\n ${declaration.split(' ').pop()} = ${source};`); + } else if (shaderType === "fragment") { + hooks += `${qualifier}IN ${declaration};\n`; + } + } + for (const hookDef in shader.hooks.helpers) { hooks += `${hookDef}${shader.hooks.helpers[hookDef]}\n`; } diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 895eb6d2b2..61f8580ac6 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -6,7 +6,7 @@ import * as constants from '../core/constants'; import { getStrokeDefs } from '../webgl/enums'; -import { DataType } from '../strands/ir_types.js'; +import { DataType, INSTANCE_ID_VARYING_NAME } from '../strands/ir_types.js'; import { colorVertexShader, colorFragmentShader } from './shaders/color'; import { lineVertexShader, lineFragmentShader} from './shaders/line'; @@ -2651,6 +2651,50 @@ ${hookUniformFields}} } } + // Handle instanceID varying for fragment access + if (shader.hooks.instanceIDVarying) { + const { name, declaration, source, interpolation } = shader.hooks.instanceIDVarying; + const nextLocIndex = this._getNextAvailableLocation(preMain, shaderType); + const interpAttr = interpolation ? ` @interpolate(${interpolation})` : ''; + const [varName, varType] = declaration.split(':').map(s => s.trim()); + const structMember = `@location(${nextLocIndex})${interpAttr} ${declaration},`; + + if (shaderType === 'vertex') { + // Inject into VertexOutput struct + preMain = preMain.replace( + /struct\s+VertexOutput\s+\{([^}]*)\}/, + (match, body) => `struct VertexOutput {${body}\n${structMember}}` + ); + // Add private global + preMain += `var ${declaration};\n`; + // Assign from built-in instanceID at start of main() + postMain = `\n ${varName} = ${source};\n` + postMain; + // Copy to output struct before return + const returnMatch = postMain.match(/return\s+(\w+)\s*;/); + if (returnMatch) { + const outputVarName = returnMatch[1]; + postMain = postMain.replace( + /(return\s+\w+\s*;)/g, + `${outputVarName}.${varName} = ${varName};\n $1` + ); + } + } else if (shaderType === 'fragment') { + // Inject into FragmentInput struct + preMain = preMain.replace( + /struct\s+FragmentInput\s+\{([^}]*)\}/, + (match, body) => `struct FragmentInput {${body}\n${structMember}}` + ); + // Add private global + preMain += `var ${declaration};\n`; + // Initialize from input struct at start of main() + const inputMatch = main.match(/fn main\s*\((\w+):\s*\w+\)/); + if (inputMatch) { + const inputVarName = inputMatch[1]; + postMain = `\n ${varName} = ${inputVarName}.${varName};\n` + postMain; + } + } + } + let hooks = ''; let defines = ''; if (shader.hooks.declarations) { diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index 5888a6d347..32226fa83d 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -1,4 +1,4 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType, DataType } from "../strands/ir_types"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType, DataType, INSTANCE_ID_VARYING_NAME } from "../strands/ir_types"; import { getNodeDataFromID, extractNodeTypeInfo } from "../strands/ir_dag"; import * as FES from '../strands/strands_FES'; import * as build from '../strands/ir_builders'; @@ -427,6 +427,12 @@ export const wgslBackend = { } } + // Detect instanceID usage in fragment context and rewrite to varying name + if (node.identifier === this.instanceIdReference() && generationContext.shaderContext === 'fragment') { + generationContext.strandsContext._instanceIDUsedInFragment = true; + return INSTANCE_ID_VARYING_NAME; + } + // Check if this is a uniform variable (but not a texture or storage buffer) const uniform = generationContext.strandsContext?.uniforms?.find(uniform => uniform.name === node.identifier); if (uniform && uniform.typeInfo.baseType !== 'sampler2D' && uniform.typeInfo.baseType !== 'storage') { @@ -584,4 +590,8 @@ export const wgslBackend = { instanceIdReference() { return 'instanceID'; }, + + generateInstanceIDVarying() { + return { name: INSTANCE_ID_VARYING_NAME, declaration: `${INSTANCE_ID_VARYING_NAME}: i32`, source: 'i32(instanceID)', interpolation: 'flat' }; + }, } diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index b88be90dd1..7e1889674e 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -1034,6 +1034,33 @@ visualSuite('WebGL', function() { p5.model(obj, 25); screenshot(); }); + visualTest('instanceID in fragment hook colors instances', (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const numInstances = 4; + const shader = p5.baseMaterialShader().modify(() => { + // Vertex hook: position instances in a horizontal row + p5.getWorldInputs((inputs) => { + const id = p5.instanceID(); + const spacing = 12; + const offset = (id - (numInstances - 1) / 2.0) * spacing; + inputs.position.x += offset; + return inputs; + }); + // Fragment hook: color each instance based on instanceID + p5.getFinalColor((color) => { + const id = p5.instanceID(); + const t = id / (numInstances - 1.0); + color = [t, t, t, 1]; + return color; + }); + }, { p5, numInstances }); + p5.background(128); + p5.noStroke(); + p5.shader(shader); + const obj = p5.buildGeometry(() => p5.circle(0, 0, 10)); + p5.model(obj, numInstances); + screenshot(); + }); }); visualSuite('p5.strands', () => { diff --git a/test/unit/visual/screenshots/WebGL/instanced randering/instanceID in fragment hook colors instances/000.png b/test/unit/visual/screenshots/WebGL/instanced randering/instanceID in fragment hook colors instances/000.png new file mode 100644 index 0000000000000000000000000000000000000000..86eb4ba3396fb893b03d3fd48dcc2979d90ded6e GIT binary patch literal 653 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2o;fFgba;IEGX(zL{#+cgTUqb*=8g zHnA)VmYtk;S_*`Bcv`ruSlB9c>+T*8xtTXp%sWkl6qIXykKLF1^SAxO)aO6Ht3U9z zd-B*R?yN`o5m`?Y#{E&9Y}P*_%2PhP5E9*`jEhsZPT4ikBT(^t@k#AB->WU>pI=+N zCvH8{sZCQh>Ab%4`|rJ3_A`B0bi}wBH{bMeTO64EFKesNhP!#JYp!R{JpX+Er};9P zU5ms5Lsq9LF&UaIlVQr+KAWLeu}R9J_hlDD>#<{x3-3x40@)2Zr&U%jFmzF1^Gjai z$Ps_B?ny@XqBmLyeg1f_>nYdKnmyZ-3RRvxwq2K@!*TJz?sbkH(>ZU< z(G%Wg87s5JGB&T{>`T=nQ78Mq+kbx(T4^Kqf2sZ6zd4h0TaQ0B3~=P|Z93q>UvO>W z%aW-7@-CT)-5jqu_KOP$En>IOo~HIT-^fecLfec((Qx&>>$A@;t6RUBJyJAy!*&Or zM>&T%7z)g$r_XwN`mLzaGKJ$zhV6$PS6sAUI&g6Z<9C%7U`VK~|991KHb)Vo(uzpE z=}ZC)4`v;>qCdTRj^A>rT%|2yf*m*CmH~yQT>m; Date: Thu, 2 Apr 2026 00:06:23 +0530 Subject: [PATCH 129/250] Add WebGPU visual test for instanceID fragment support --- test/unit/visual/cases/webgpu.js | 29 ++++++++++++++++++ .../000.png | Bin 0 -> 398 bytes .../metadata.json | 3 ++ 3 files changed, 32 insertions(+) create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/instanceID in fragment hook colors instances (WebGPU)/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/instanceID in fragment hook colors instances (WebGPU)/metadata.json diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 2b3939bf03..fb916c2b35 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -272,8 +272,37 @@ visualSuite("WebGPU", function () { p5.filter(invert); await screenshot(); }); + + visualTest('instanceID in fragment hook colors instances (WebGPU)', async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + const numInstances = 4; + const shader = p5.baseMaterialShader().modify(() => { + // Vertex hook: position instances in a horizontal row + p5.getWorldInputs((inputs) => { + const id = p5.instanceID(); + const spacing = 12; + const offset = (id - (numInstances - 1) / 2.0) * spacing; + inputs.position.x += offset; + return inputs; + }); + // Fragment hook: color each instance based on instanceID + p5.getFinalColor((color) => { + const id = p5.instanceID(); + const t = id / (numInstances - 1.0); + color = [t, t, t, 1]; + return color; + }); + }, { p5, numInstances }); + p5.background(128); + p5.noStroke(); + p5.shader(shader); + const obj = p5.buildGeometry(() => p5.circle(0, 0, 10)); + p5.model(obj, numInstances); + await screenshot(); + }); }); + visualSuite('filters', function() { const setupSketch = async (p5) => { await p5.createCanvas(50, 50, p5.WEBGPU); diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/instanceID in fragment hook colors instances (WebGPU)/000.png b/test/unit/visual/screenshots/WebGPU/Shaders/instanceID in fragment hook colors instances (WebGPU)/000.png new file mode 100644 index 0000000000000000000000000000000000000000..1e99dcacc9340e20edfbda7616a51445d4ce68ad GIT binary patch literal 398 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2o;fFgkm>IEGX(z6rm`)ndRS{oV4{ z#8>X;?(f`fv*yYsBXbR(la3d)3mnaau(A)9n}7P><}bBYp!l5TbDfNr(^EaAbpLM4^<2Dcze&)v?N0srY-{d4 zKK&`|{m0bG)$2csE0;a+{C|`qC|`G-smI=>*QJ)+yOhlvl>c(8ThErZw0b|~&88~7 zFTUT84$8h(`ZeWH_SwpLhpNq^ie=i>h2yf1blB9sID3>sh<%NCLBrFD76sGYA2nQU zX4dIH(rEqXsE6c%OH!@h*BLB+_>!xCsVUd{*hII*%T`OR`!NwAcUu literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/instanceID in fragment hook colors instances (WebGPU)/metadata.json b/test/unit/visual/screenshots/WebGPU/Shaders/instanceID in fragment hook colors instances (WebGPU)/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Shaders/instanceID in fragment hook colors instances (WebGPU)/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file From 4e5f1b43ac4b1bc24bc602e9e19f078fda1cc042 Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Thu, 2 Apr 2026 06:24:58 +0530 Subject: [PATCH 130/250] Add auto spreading for WebGPU compute dispatches and fix strands void return handling --- src/strands/strands_api.js | 14 ++++++---- src/strands/strands_codegen.js | 5 +--- src/webgpu/p5.RendererWebGPU.js | 43 ++++++++++++++++++++++++++++--- src/webgpu/shaders/compute.js | 16 +++++++----- src/webgpu/strands_wgslBackend.js | 10 ++++--- 5 files changed, 66 insertions(+), 22 deletions(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index ab1453eb1c..18fac44ecd 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -219,7 +219,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const nodeData = DAG.createNodeData({ nodeType: NodeType.STATEMENT, statementType: StatementType.EARLY_RETURN, - dependsOn: [valueNode.id] + dependsOn: value !== undefined ? [valueNode.id] : [] }); const earlyReturnID = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, earlyReturnID); @@ -786,17 +786,21 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { return newStruct.id; } } + else if (!expectedReturnType.dataType || expectedReturnType.typeName?.trim() === 'void') { + return null; + } else /*if(isNativeType(expectedReturnType.typeName))*/ { - if (!expectedReturnType.dataType) { - throw new Error(`Missing dataType for return type ${expectedReturnType.typeName}`); - } const expectedTypeInfo = expectedReturnType.dataType; return enforceReturnTypeMatch(strandsContext, expectedTypeInfo, retNode, hookType.name); } } for (const { valueNode, earlyReturnID } of hook.earlyReturns) { const id = handleRetVal(valueNode); - dag.dependsOn[earlyReturnID] = [id]; + if (id !== null) { + dag.dependsOn[earlyReturnID] = [id]; + } else { + dag.dependsOn[earlyReturnID] = []; + } } rootNodeID = userReturned ? handleRetVal(userReturned) : undefined; const fullHookName = `${hookType.returnType.typeName} ${hookType.name}`; diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index bf1ea61b4d..38e24c511e 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -64,12 +64,9 @@ export function generateShaderCode(strandsContext) { let returnType; if (hookType.returnType.properties) { returnType = structType(hookType.returnType); - } else if (hookType.returnType.typeName === 'void') { + } else if (!hookType.returnType.dataType || hookType.returnType.typeName?.trim() === 'void') { returnType = null; } else { - if (!hookType.returnType.dataType) { - throw new Error(`Missing dataType for return type ${hookType.returnType.typeName}`); - } returnType = hookType.returnType.dataType; } diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 61f8580ac6..05f7167a91 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -3813,10 +3813,45 @@ ${hookUniformFields}} const WORKGROUP_SIZE_Y = 8; const WORKGROUP_SIZE_Z = 1; - // Calculate number of workgroups needed - const workgroupCountX = Math.ceil(x / WORKGROUP_SIZE_X); - const workgroupCountY = Math.ceil(y / WORKGROUP_SIZE_Y); - const workgroupCountZ = Math.ceil(z / WORKGROUP_SIZE_Z); + // auto spreading: if any dimension is too large or for performance optimization, + // spread total iteration count across dimensions + const totalIterations = x * y * z; + const MAX_THREADS_PER_DIM = 65535 * 8; + + let px = x; + let py = y; + let pz = z; + + // we spread if we exceed GPU limits OR if it involves a large 1D dispatch + const exceedsLimits = x > MAX_THREADS_PER_DIM || y > MAX_THREADS_PER_DIM || z > MAX_THREADS_PER_DIM; + const isLarge1D = totalIterations > 1024 && y === 1 && z === 1; + + if (exceedsLimits || isLarge1D) { + if (totalIterations > 1000000) { + // 3D cube type for extreme large counts + px = Math.ceil(Math.pow(totalIterations, 1 / 3)); + py = Math.ceil(Math.pow(totalIterations, 1 / 3)); + pz = Math.ceil(totalIterations / (px * py)); + } else { + // 2D square type for moderate large counts + px = Math.ceil(Math.sqrt(totalIterations)); + py = Math.ceil(totalIterations / px); + pz = 1; + } + + if (p5.debug || exceedsLimits) { + console.warn( + `p5.js: Compute dispatch (${x}, ${y}, ${z}) auto-spread to (${px}, ${py}, ${pz}) ` + + `to ${exceedsLimits ? 'stay within GPU limits' : 'optimize performance'}.` + ); + } + } + + shader.setUniform('uPhysicalCount', [px, py, pz]); + + const workgroupCountX = Math.ceil(px / WORKGROUP_SIZE_X); + const workgroupCountY = Math.ceil(py / WORKGROUP_SIZE_Y); + const workgroupCountZ = Math.ceil(pz / WORKGROUP_SIZE_Z); const commandEncoder = this.device.createCommandEncoder(); const passEncoder = commandEncoder.beginComputePass(); diff --git a/src/webgpu/shaders/compute.js b/src/webgpu/shaders/compute.js index 39e6146f4e..dafe356ee6 100644 --- a/src/webgpu/shaders/compute.js +++ b/src/webgpu/shaders/compute.js @@ -1,6 +1,7 @@ export const baseComputeShader = ` struct ComputeUniforms { uTotalCount: vec3, + uPhysicalCount: vec3, } @group(0) @binding(0) var uniforms: ComputeUniforms; @@ -11,16 +12,19 @@ fn main( @builtin(workgroup_id) workgroupId: vec3, @builtin(local_invocation_index) localIndex: u32 ) { - var index = vec3(globalId); + let totalIterations = u32(uniforms.uTotalCount.x) * u32(uniforms.uTotalCount.y) * u32(uniforms.uTotalCount.z); + let physicalId = globalId.x + globalId.y * (u32(uniforms.uPhysicalCount.x)) + globalId.z * (u32(uniforms.uPhysicalCount.x) * u32(uniforms.uPhysicalCount.y)); - if ( - index.x >= uniforms.uTotalCount.x || - index.y >= uniforms.uTotalCount.y || - index.z >= uniforms.uTotalCount.z - ) { + if (physicalId >= totalIterations) { return; } + var index = vec3(0); + index.x = i32(physicalId % u32(uniforms.uTotalCount.x)); + let remainingY = physicalId / u32(uniforms.uTotalCount.x); + index.y = i32(remainingY % u32(uniforms.uTotalCount.y)); + index.z = i32(remainingY / u32(uniforms.uTotalCount.y)); + HOOK_iteration(index); } `; diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index 32226fa83d..1a5182cde8 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -301,9 +301,13 @@ export const wgslBackend = { // Generate just a semicolon (unless suppressed) generationContext.write(semicolon); } else if (node.statementType === StatementType.EARLY_RETURN) { - const exprNodeID = node.dependsOn[0]; - const expr = this.generateExpression(generationContext, dag, exprNodeID); - generationContext.write(`return ${expr}${semicolon}`); + if (node.dependsOn && node.dependsOn.length > 0) { + const exprNodeID = node.dependsOn[0]; + const expr = this.generateExpression(generationContext, dag, exprNodeID); + generationContext.write(`return ${expr}${semicolon}`); + } else { + generationContext.write(`return${semicolon}`); + } } }, generateAssignment(generationContext, dag, nodeID) { From cf9141c90c066e651f98d0b21d3b041f0ebfcca4 Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Thu, 2 Apr 2026 22:21:12 +0530 Subject: [PATCH 131/250] fix: add filterColor alias for 2D filter shaders --- src/image/filterRenderer2D.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/image/filterRenderer2D.js b/src/image/filterRenderer2D.js index d6bf72eed3..0f9e756f91 100644 --- a/src/image/filterRenderer2D.js +++ b/src/image/filterRenderer2D.js @@ -299,7 +299,10 @@ class FilterRenderer2D { 'vec4 getColor': `(FilterInputs inputs, in sampler2D canvasContent) { return getTexture(canvasContent, inputs.texCoord); }` - } + }, + hookAliases: { + 'getColor': ['filterColor'], + }, } ); } From 2e3b38d35977775dc036eb0e63bec6f05b688fd5 Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Fri, 3 Apr 2026 02:50:26 +0530 Subject: [PATCH 132/250] fix: allow setup() to return Promise for async workflows --- src/core/main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/main.js b/src/core/main.js index 4ce9d91c55..9a058b6e3d 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -673,6 +673,7 @@ export default p5; * * @method setup * @for p5 + * @return {void|Promise} * * @example * function setup() { From ebf64c8400faf65c35c7c6c236a756a8f9ef4848 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 3 Apr 2026 21:01:36 -0400 Subject: [PATCH 133/250] Fix feedback in webgpu --- preview/index.html | 245 +++----------------------------- src/webgpu/p5.RendererWebGPU.js | 33 ++++- 2 files changed, 46 insertions(+), 232 deletions(-) diff --git a/preview/index.html b/preview/index.html index 055f642f2e..e1b0eb188f 100644 --- a/preview/index.html +++ b/preview/index.html @@ -23,237 +23,28 @@ p5.registerAddon(rendererWebGPU); const sketch = function (p) { - let fbo; - let sh, sh2; - let ssh; - let tex; - let font; - let redFilter; - let env; - let instance; - - // Compute shader variables - let computeShader; - let particleBuffer; - let bouncingCirclesShader; - let circleGeometry; - const NUM_CIRCLES = 100; - const RADIUS = 2; - - p.setup = async function () { + p.setup = async function() { await p.createCanvas(400, 400, p.WEBGPU); - env = await p.loadImage('img/spheremap.jpg'); - font = await p.loadFont( - 'font/PlayfairDisplay.ttf' - ); - fbo = p.createFramebuffer(); - - instance = p.buildGeometry(() => p.sphere(5)); - circleGeometry = p.buildGeometry(() => p.sphere(RADIUS)); - - redFilter = p.baseFilterShader().modify(() => { - p.getColor((inputs, canvasContent) => { - let col = p.getTexture(canvasContent, inputs.texCoord); - col.g = col.r; - col.b = col.r; - return col; - }) - }, { p }) - - tex = p.createImage(100, 100); - tex.loadPixels(); - for (let x = 0; x < tex.width; x++) { - for (let y = 0; y < tex.height; y++) { - const off = (x + y * tex.width) * 4; - tex.pixels[off] = p.round((x / tex.width) * 255); - tex.pixels[off + 1] = p.round((y / tex.height) * 255); - tex.pixels[off + 2] = 0; - tex.pixels[off + 3] = 255; - } - } - tex.updatePixels(); - fbo.draw(() => { - p.imageMode(p.CENTER); - p.image(tex, 0, 0, p.width, p.height); - }); - - sh = p.baseMaterialShader().modify(() => { - const time = p.uniformFloat(() => p.millis()); - p.getWorldInputs((inputs) => { - inputs.position.y += 40 * p.sin(time * 0.005); - return inputs; - }); - }, { p }) - sh2 = p.baseMaterialShader().modify(() => { - p.getWorldInputs((inputs) => { - inputs.position.x += 20 * p.instanceID(); - return inputs; - }); - }, { p }) - - // Initialize storage buffers with random positions and velocities - const initialParticles = []; - for (let i = 0; i < NUM_CIRCLES; i++) { - initialParticles.push({ - position: [p.random(-150, 150), p.random(-150, 150)], - velocity: [ - 0.1 * p.random(1, 3) * (p.random() > 0.5 ? 1 : -1), - 0.1 * p.random(1, 3) * (p.random() > 0.5 ? 1 : -1) - ] - }) - } - - particleBuffer = p.createStorage(initialParticles); - - // Create compute shader for physics simulation - computeShader = p.buildComputeShader(() => { - const particles = p.uniformStorage('particles', particleBuffer); - const bounds = p.uniformVec2(() => [p.width / 2 - RADIUS, p.height / 2 - RADIUS]); - const deltaTime = p.uniformFloat(() => p.deltaTime * 0.1); - - const idx = p.index.x; - - // Read current position and velocity - let position = particles[idx].position; - let velocity = particles[idx].velocity; - - // Update position - position += velocity * deltaTime; - - // Bounce off boundaries - if (position.x > bounds.x || position.x < -bounds.x) { - velocity.x = -velocity.x; - position.x = p.clamp(position.x, -bounds.x, bounds.x); - } - if (position.y > bounds.y || position.y < -bounds.y) { - velocity.y = -velocity.y; - position.y = p.clamp(position.y, -bounds.y, bounds.y); - } - - particles[idx].position = position; - particles[idx].velocity = velocity; - }, { p, RADIUS, particleBuffer }); - - // Shader for rendering bouncing circles from storage buffer - bouncingCirclesShader = p.baseMaterialShader().modify(() => { - const particles = p.uniformStorage('particles', particleBuffer); - - p.getWorldInputs((inputs) => { - const instanceIdx = p.instanceID(); - inputs.position.xy += particles[instanceIdx].position; - return inputs; - }); - }, { p, particleBuffer }); - }; - - p.draw = function () { - // Run compute shader to update physics + p.background(0) + } + p.draw = function() { debugger - p.compute(computeShader, NUM_CIRCLES); - - p.clear(); - p.rotateY(p.millis() * 0.001); - p.push(); - //p.clip(() => p.rect(-50, -50, 200, 200)); - /*p.orbitControl(); - p.push(); - p.textAlign(p.CENTER, p.CENTER); - p.textFont(font); - p.textSize(85) - p.fill('red') - p.noStroke() - p.rect(0, 0, 100, 100); - p.fill(0); p.push() - p.rotate(p.millis() * 0.001) - p.text('Hello!', 0, 0); + p.fill(0, 1) + p.noStroke() + //p.plane(p.width, p.height) + p.rectMode(p.CENTER) + p.rect(0, 0, p.width, p.height) p.pop() - p.pop(); - return;*/ - p.orbitControl(); - const t = p.millis() * 0.002; - p.background(200); - p.panorama(env); - p.push(); - p.imageLight(env); - p.shader(sh); - // p.strokeShader(ssh) - p.ambientLight(10); - //p.directionalLight(100, 100, 100, 0, 1, -1); - //p.pointLight(155, 155, 155, 0, -200, 500); - p.specularMaterial(255); - p.shininess(50); - p.metalness(100); - //p.stroke('white'); - p.noStroke(); - for (const [i, c] of ['red', 'gray', 'blue'].entries()) { - p.push(); - p.fill(c); - p.translate( - p.width/3 * p.sin(t + i * Math.E), - 0, //p.width/3 * p.sin(t * 0.9 + i * Math.E + 0.2), - p.width/3 * p.sin(t * 1.2 + i * Math.E + 0.3), - ) - if (i % 2 === 0) { - if (i === 0) { - p.texture(fbo) - } - p.box(30); - } else { - p.sphere(30); - } - p.pop(); - } - p.pop(); - - p.push(); - p.shader(sh2); - p.noStroke(); - p.fill('red'); - p.model(instance, 10); - p.pop(); - - // Draw compute shader-driven bouncing circles - p.push(); - p.shader(bouncingCirclesShader); - p.noStroke(); - p.fill('#4ECDC4'); - p.model(circleGeometry, NUM_CIRCLES); - p.pop(); - - // Test beginShape/endShape with immediate mode shapes - p.push(); - p.translate(0, 100, 0); - p.fill('yellow'); - p.noStroke(); - - // Draw a circle using beginShape/endShape - p.beginShape(); - const numPoints = 16; - for (let i = 0; i < numPoints; i++) { - const angle = (i / numPoints) * Math.PI * 2; - const x = Math.cos(angle) * 50; - const y = Math.sin(angle) * 50; - p.vertex(x, y); - } - p.endShape(p.CLOSE); - - p.translate(100, 0, 0); - p.fill('purple'); - - // Draw a square using beginShape/endShape - p.beginShape(); - p.vertex(-30, -30); - p.vertex(30, -30); - p.vertex(30, 30); - p.vertex(-30, 30); - p.endShape(p.CLOSE); - - p.pop(); - - // p.filter(p.BLUR, 10) - p.pop(); - }; + + p.fill(255) + p.noStroke() + p.circle( + 100 * p.sin(p.frameCount * 0.1), + 0, + 50 + ) + } }; new p5(sketch); diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 61f8580ac6..2b616e4a63 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -454,9 +454,21 @@ function rendererWebGPU(p5, fn) { const _b = args[2] || 0; const _a = args[3] || 0; - // If PENDING and no custom framebuffer, clear means stay UNPROMOTED - if (this._frameState === FRAME_STATE.PENDING && !this.activeFramebuffer()) { - this._frameState = FRAME_STATE.UNPROMOTED; + // If PENDING and no custom framebuffer, clear means stay UNPROMOTED. + // However, if we are still in setup (frameCount == 0), we must promote + // so that mainFramebuffer gets the cleared content. This ensures that if + // draw() later promotes without a copy, it starts from the correct state + // rather than a stale mainFramebuffer. + // Note: a mid-draw-loop transition from UNPROMOTED back to PROMOTED + // (i.e. calling background() some frames but not others) will still + // lose intermediate UNPROMOTED frame content. + if (this._frameState !== FRAME_STATE.PROMOTED && !this.activeFramebuffer()) { + if (this._pInst.frameCount > 0) { + this._frameState = FRAME_STATE.UNPROMOTED; + } else { + this._promoteToFramebufferWithoutCopy(); + // clear() then targets mainFramebuffer via activeFramebuffer() + } } this._finishActiveRenderPass(); @@ -1143,8 +1155,11 @@ function rendererWebGPU(p5, fn) { _resetBuffersBeforeDraw() { this._finishActiveRenderPass(); + // Set state to PENDING - we'll decide on first draw - this._frameState = FRAME_STATE.PENDING; + if (this._pInst.frameCount > 0) { + this._frameState = FRAME_STATE.PENDING; + } // Clear depth buffer but DON'T start any render pass yet const activeFramebuffer = this.activeFramebuffer(); @@ -1255,6 +1270,8 @@ function rendererWebGPU(p5, fn) { // once we're drawing to the framebuffer, because normally // those are reset. const savedModelMatrix = this.states.uModelMatrix.copy(); + this.states.uModelMatrix.set(this.states.uModelMatrix.copy()); + this.states.uModelMatrix.reset(); this.mainFramebuffer.defaultCamera.set(this.states.curCamera); this.mainFramebuffer.begin(); @@ -1263,6 +1280,11 @@ function rendererWebGPU(p5, fn) { } _promoteToFramebufferWithoutCopy() { + // Already promoted this frame + if (this._frameState === FRAME_STATE.PROMOTED) { + return; + } + // Ensure mainFramebuffer matches canvas size if (this.mainFramebuffer.width !== this.width || this.mainFramebuffer.height !== this.height) { @@ -1277,6 +1299,8 @@ function rendererWebGPU(p5, fn) { // Preserve transformation state const savedModelMatrix = this.states.uModelMatrix.copy(); + this.states.uModelMatrix.set(this.states.uModelMatrix.copy()); + this.states.uModelMatrix.reset(); this.mainFramebuffer.defaultCamera.set(this.states.curCamera); // Begin rendering to mainFramebuffer @@ -1590,7 +1614,6 @@ function rendererWebGPU(p5, fn) { } this.flushDraw(); - // this._pInst.background('red'); this._pInst.push(); this.states.setValue('enableLighting', false); this.states.setValue('activeImageLight', null); From 814ad0772daaa5bf382429bc0fba8d1110c9dda2 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 3 Apr 2026 21:08:53 -0400 Subject: [PATCH 134/250] Add test for feedback --- test/unit/visual/cases/webgpu.js | 29 ++++++++++++++++++ .../000.png | Bin 0 -> 390 bytes .../metadata.json | 3 ++ 3 files changed, 32 insertions(+) create mode 100644 test/unit/visual/screenshots/WebGPU/Feedback/Drawing accumulates across frames when background is set in setup/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Feedback/Drawing accumulates across frames when background is set in setup/metadata.json diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index fb916c2b35..93858f03c3 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -1423,4 +1423,33 @@ visualSuite("WebGPU", function () { } ); }); + + visualSuite('Feedback', function() { + visualTest( + 'Drawing accumulates across frames when background is set in setup', + async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + // Set an initial background before the draw loop starts. + // This should persist into the first draw frame. + p5.background('red'); + + return new Promise(resolve => { + let frame = 0; + p5.draw = function() { + // Draw circles without clearing, so they accumulate + p5.noStroke(); + p5.fill('blue'); + p5.circle(-15 + frame * 15, 0, 10); + frame++; + if (frame >= 3) { + p5.noLoop(); + screenshot().then(resolve); + } + }; + p5.loop(); + }); + } + ); + }); }); diff --git a/test/unit/visual/screenshots/WebGPU/Feedback/Drawing accumulates across frames when background is set in setup/000.png b/test/unit/visual/screenshots/WebGPU/Feedback/Drawing accumulates across frames when background is set in setup/000.png new file mode 100644 index 0000000000000000000000000000000000000000..8f406fd13da1abc9576926811e678b1209c3412c GIT binary patch literal 390 zcmV;10eSw3P)b@pShU4iuuXmOJzBefN&M%0K?0{Uq= z1)F`X8p)2%iKs@Lg3Z2GjbumXL{uYA!De5pMzW)GBK}7s3p(_e(O`XH!UzKP`q;gi z8*PMwX@Y`wg(C(KoZ(<`el^!LLLn3!)5=+BFg#a^YZ_Vb2(_8d69#j?6gOg|Gew4* zu|{J1-dxj2_@%${Y#QmX;B1ZC`^!30ifbAfA|fr}-+QIFrV*wZOPs2&dxv{aTWK_$eGxh<}y-K8`Ppk zm_kiZBQ>=_Eoy`*)P!0aNzO<{k|T2_s1fIYldo4J$&on|)QEGy$=BbektYBE0RR72 k$QUvJ000I_L_t&o0HRab0T>5l$p8QV07*qoM6N<$f@Vvgg8%>k literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Feedback/Drawing accumulates across frames when background is set in setup/metadata.json b/test/unit/visual/screenshots/WebGPU/Feedback/Drawing accumulates across frames when background is set in setup/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Feedback/Drawing accumulates across frames when background is set in setup/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file From b8737a78fa397dc3ce2d2f4c2f3840d3fb483f81 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 3 Apr 2026 21:09:50 -0400 Subject: [PATCH 135/250] Reset test sketch --- preview/index.html | 245 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 227 insertions(+), 18 deletions(-) diff --git a/preview/index.html b/preview/index.html index e1b0eb188f..055f642f2e 100644 --- a/preview/index.html +++ b/preview/index.html @@ -23,28 +23,237 @@ p5.registerAddon(rendererWebGPU); const sketch = function (p) { - p.setup = async function() { + let fbo; + let sh, sh2; + let ssh; + let tex; + let font; + let redFilter; + let env; + let instance; + + // Compute shader variables + let computeShader; + let particleBuffer; + let bouncingCirclesShader; + let circleGeometry; + const NUM_CIRCLES = 100; + const RADIUS = 2; + + p.setup = async function () { await p.createCanvas(400, 400, p.WEBGPU); - p.background(0) - } - p.draw = function() { + env = await p.loadImage('img/spheremap.jpg'); + font = await p.loadFont( + 'font/PlayfairDisplay.ttf' + ); + fbo = p.createFramebuffer(); + + instance = p.buildGeometry(() => p.sphere(5)); + circleGeometry = p.buildGeometry(() => p.sphere(RADIUS)); + + redFilter = p.baseFilterShader().modify(() => { + p.getColor((inputs, canvasContent) => { + let col = p.getTexture(canvasContent, inputs.texCoord); + col.g = col.r; + col.b = col.r; + return col; + }) + }, { p }) + + tex = p.createImage(100, 100); + tex.loadPixels(); + for (let x = 0; x < tex.width; x++) { + for (let y = 0; y < tex.height; y++) { + const off = (x + y * tex.width) * 4; + tex.pixels[off] = p.round((x / tex.width) * 255); + tex.pixels[off + 1] = p.round((y / tex.height) * 255); + tex.pixels[off + 2] = 0; + tex.pixels[off + 3] = 255; + } + } + tex.updatePixels(); + fbo.draw(() => { + p.imageMode(p.CENTER); + p.image(tex, 0, 0, p.width, p.height); + }); + + sh = p.baseMaterialShader().modify(() => { + const time = p.uniformFloat(() => p.millis()); + p.getWorldInputs((inputs) => { + inputs.position.y += 40 * p.sin(time * 0.005); + return inputs; + }); + }, { p }) + sh2 = p.baseMaterialShader().modify(() => { + p.getWorldInputs((inputs) => { + inputs.position.x += 20 * p.instanceID(); + return inputs; + }); + }, { p }) + + // Initialize storage buffers with random positions and velocities + const initialParticles = []; + for (let i = 0; i < NUM_CIRCLES; i++) { + initialParticles.push({ + position: [p.random(-150, 150), p.random(-150, 150)], + velocity: [ + 0.1 * p.random(1, 3) * (p.random() > 0.5 ? 1 : -1), + 0.1 * p.random(1, 3) * (p.random() > 0.5 ? 1 : -1) + ] + }) + } + + particleBuffer = p.createStorage(initialParticles); + + // Create compute shader for physics simulation + computeShader = p.buildComputeShader(() => { + const particles = p.uniformStorage('particles', particleBuffer); + const bounds = p.uniformVec2(() => [p.width / 2 - RADIUS, p.height / 2 - RADIUS]); + const deltaTime = p.uniformFloat(() => p.deltaTime * 0.1); + + const idx = p.index.x; + + // Read current position and velocity + let position = particles[idx].position; + let velocity = particles[idx].velocity; + + // Update position + position += velocity * deltaTime; + + // Bounce off boundaries + if (position.x > bounds.x || position.x < -bounds.x) { + velocity.x = -velocity.x; + position.x = p.clamp(position.x, -bounds.x, bounds.x); + } + if (position.y > bounds.y || position.y < -bounds.y) { + velocity.y = -velocity.y; + position.y = p.clamp(position.y, -bounds.y, bounds.y); + } + + particles[idx].position = position; + particles[idx].velocity = velocity; + }, { p, RADIUS, particleBuffer }); + + // Shader for rendering bouncing circles from storage buffer + bouncingCirclesShader = p.baseMaterialShader().modify(() => { + const particles = p.uniformStorage('particles', particleBuffer); + + p.getWorldInputs((inputs) => { + const instanceIdx = p.instanceID(); + inputs.position.xy += particles[instanceIdx].position; + return inputs; + }); + }, { p, particleBuffer }); + }; + + p.draw = function () { + // Run compute shader to update physics debugger - p.push() - p.fill(0, 1) + p.compute(computeShader, NUM_CIRCLES); + + p.clear(); + p.rotateY(p.millis() * 0.001); + p.push(); + //p.clip(() => p.rect(-50, -50, 200, 200)); + /*p.orbitControl(); + p.push(); + p.textAlign(p.CENTER, p.CENTER); + p.textFont(font); + p.textSize(85) + p.fill('red') p.noStroke() - //p.plane(p.width, p.height) - p.rectMode(p.CENTER) - p.rect(0, 0, p.width, p.height) + p.rect(0, 0, 100, 100); + p.fill(0); + p.push() + p.rotate(p.millis() * 0.001) + p.text('Hello!', 0, 0); p.pop() - - p.fill(255) - p.noStroke() - p.circle( - 100 * p.sin(p.frameCount * 0.1), - 0, - 50 - ) - } + p.pop(); + return;*/ + p.orbitControl(); + const t = p.millis() * 0.002; + p.background(200); + p.panorama(env); + p.push(); + p.imageLight(env); + p.shader(sh); + // p.strokeShader(ssh) + p.ambientLight(10); + //p.directionalLight(100, 100, 100, 0, 1, -1); + //p.pointLight(155, 155, 155, 0, -200, 500); + p.specularMaterial(255); + p.shininess(50); + p.metalness(100); + //p.stroke('white'); + p.noStroke(); + for (const [i, c] of ['red', 'gray', 'blue'].entries()) { + p.push(); + p.fill(c); + p.translate( + p.width/3 * p.sin(t + i * Math.E), + 0, //p.width/3 * p.sin(t * 0.9 + i * Math.E + 0.2), + p.width/3 * p.sin(t * 1.2 + i * Math.E + 0.3), + ) + if (i % 2 === 0) { + if (i === 0) { + p.texture(fbo) + } + p.box(30); + } else { + p.sphere(30); + } + p.pop(); + } + p.pop(); + + p.push(); + p.shader(sh2); + p.noStroke(); + p.fill('red'); + p.model(instance, 10); + p.pop(); + + // Draw compute shader-driven bouncing circles + p.push(); + p.shader(bouncingCirclesShader); + p.noStroke(); + p.fill('#4ECDC4'); + p.model(circleGeometry, NUM_CIRCLES); + p.pop(); + + // Test beginShape/endShape with immediate mode shapes + p.push(); + p.translate(0, 100, 0); + p.fill('yellow'); + p.noStroke(); + + // Draw a circle using beginShape/endShape + p.beginShape(); + const numPoints = 16; + for (let i = 0; i < numPoints; i++) { + const angle = (i / numPoints) * Math.PI * 2; + const x = Math.cos(angle) * 50; + const y = Math.sin(angle) * 50; + p.vertex(x, y); + } + p.endShape(p.CLOSE); + + p.translate(100, 0, 0); + p.fill('purple'); + + // Draw a square using beginShape/endShape + p.beginShape(); + p.vertex(-30, -30); + p.vertex(30, -30); + p.vertex(30, 30); + p.vertex(-30, 30); + p.endShape(p.CLOSE); + + p.pop(); + + // p.filter(p.BLUR, 10) + p.pop(); + }; }; new p5(sketch); From fad59601332055e7c8263f9583168d96b2ab2324 Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Sat, 4 Apr 2026 11:25:57 +0530 Subject: [PATCH 136/250] Add tests for void compute hook early returns --- test/unit/webgpu/p5.Shader.js | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/unit/webgpu/p5.Shader.js b/test/unit/webgpu/p5.Shader.js index 9c4a1fa559..6571d92edb 100644 --- a/test/unit/webgpu/p5.Shader.js +++ b/test/unit/webgpu/p5.Shader.js @@ -1228,5 +1228,46 @@ suite('WebGPU p5.Shader', function() { }); } }); + + suite('compute shaders', () => { + test('handle early return in void compute hook', async () => { + await myp5.createCanvas(5, 5, myp5.WEBGPU); + + // This test verifies that buildComputeShader and p5.compute + // correctly handle void hooks with early returns without crashing + // the strands compiler or hitting type errors. + expect(() => { + const computeShader = myp5.buildComputeShader(() => { + const id = myp5.index.x; + if (id > 10) { + return; // Early return in void hook + } + }, { myp5 }); + + myp5.compute(computeShader, 1); + }).not.toThrow(); + }); + + test('early return in void compute hook stops execution', async () => { + await myp5.createCanvas(5, 5, myp5.WEBGPU); + const data = myp5.createStorage([0]); + + const computeShader = myp5.buildComputeShader(() => { + const buf = myp5.uniformStorage(); + const id = myp5.index.x; + if (id == 0) { + buf[0] = 1.0; + return; + buf[0] = 2.0; // Should not execute + } + }, { myp5 }); + + computeShader.setUniform('buf', data); + + expect(() => { + myp5.compute(computeShader, 1); + }).not.toThrow(); + }); + }); }); }); From 2dba73922f9e0d1a39736fc45bf28eb268bbb094 Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Sat, 4 Apr 2026 23:39:38 +0530 Subject: [PATCH 137/250] Refactor: move getNoiseShaderSnippet to strands backend --- src/image/filterRenderer2D.js | 4 ---- src/strands/strands_api.js | 2 +- src/webgl/p5.RendererGL.js | 5 ----- src/webgl/strands_glslBackend.js | 4 ++++ src/webgpu/p5.RendererWebGPU.js | 4 ---- src/webgpu/strands_wgslBackend.js | 5 +++++ 6 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/image/filterRenderer2D.js b/src/image/filterRenderer2D.js index 0f9e756f91..2896ed392e 100644 --- a/src/image/filterRenderer2D.js +++ b/src/image/filterRenderer2D.js @@ -17,7 +17,6 @@ import filterBaseVert from '../webgl/shaders/filters/base.vert'; import webgl2CompatibilityShader from '../webgl/shaders/webgl2Compatibility.glsl'; import { glslBackend } from '../webgl/strands_glslBackend'; import { getShaderHookTypes } from '../webgl/shaderHookUtils'; -import noiseGLSL from '../webgl/shaders/functions/noise3DGLSL.glsl'; import { makeFilterShader } from '../core/filterShaders'; class FilterRenderer2D { @@ -309,9 +308,6 @@ class FilterRenderer2D { return this._baseFilterShader; } - getNoiseShaderSnippet() { - return noiseGLSL; - } /** * Set the current filter operation and parameter. If a customShader is provided, diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index ab1453eb1c..f486c51e41 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -337,7 +337,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return originalNoise.apply(this, args); // fallback to regular p5.js noise } // Get noise shader snippet from the current renderer - const noiseSnippet = this._renderer.getNoiseShaderSnippet(); + const noiseSnippet = strandsContext.backend.getNoiseShaderSnippet(); strandsContext.vertexDeclarations.add(noiseSnippet); strandsContext.fragmentDeclarations.add(noiseSnippet); strandsContext.computeDeclarations.add(noiseSnippet); diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index b744baff19..63ee001964 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -19,7 +19,6 @@ import { Image } from '../image/p5.Image'; import { glslBackend } from './strands_glslBackend'; import { TypeInfoFromGLSLName } from '../strands/ir_types.js'; import { getShaderHookTypes } from './shaderHookUtils'; -import noiseGLSL from './shaders/functions/noise3DGLSL.glsl'; import filterBaseVert from "./shaders/filters/base.vert"; import lightingShader from "./shaders/lighting.glsl"; @@ -1905,10 +1904,6 @@ class RendererGL extends Renderer3D { } } - getNoiseShaderSnippet() { - return noiseGLSL; - } - } function rendererGL(p5, fn) { diff --git a/src/webgl/strands_glslBackend.js b/src/webgl/strands_glslBackend.js index d850aef91d..d0715f53d4 100644 --- a/src/webgl/strands_glslBackend.js +++ b/src/webgl/strands_glslBackend.js @@ -1,3 +1,4 @@ +import noiseGLSL from './shaders/functions/noise3DGLSL.glsl'; import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType, DataType, INSTANCE_ID_VARYING_NAME } from "../strands/ir_types"; import { getNodeDataFromID, extractNodeTypeInfo } from "../strands/ir_dag"; import * as FES from '../strands/strands_FES'; @@ -169,6 +170,9 @@ export const glslBackend = { }).join(', ')}) {`; return firstLine; }, + getNoiseShaderSnippet() { + return noiseGLSL; + }, getTypeName(baseType, dimension) { const primitiveTypeName = TypeNames[baseType + dimension] if (!primitiveTypeName) { diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 2b616e4a63..66d9d0e12d 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -14,7 +14,6 @@ import { materialVertexShader, materialFragmentShader } from './shaders/material import { fontVertexShader, fontFragmentShader } from './shaders/font'; import { blitVertexShader, blitFragmentShader } from './shaders/blit'; import { wgslBackend } from './strands_wgslBackend'; -import noiseWGSL from './shaders/functions/noise3DWGSL'; import { baseFilterVertexShader, baseFilterFragmentShader } from './shaders/filters/base'; import { imageLightVertexShader, imageLightDiffusedFragmentShader, imageLightSpecularFragmentShader } from './shaders/imageLight'; import { baseComputeShader } from './shaders/compute'; @@ -3673,9 +3672,6 @@ ${hookUniformFields}} return super.filter(...args); } - getNoiseShaderSnippet() { - return noiseWGSL; - } baseFilterShader() { diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index 32226fa83d..3aef6778b6 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -1,3 +1,4 @@ +import noiseWGSL from './shaders/functions/noise3DWGSL.js'; import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType, DataType, INSTANCE_ID_VARYING_NAME } from "../strands/ir_types"; import { getNodeDataFromID, extractNodeTypeInfo } from "../strands/ir_dag"; import * as FES from '../strands/strands_FES'; @@ -263,6 +264,10 @@ export const wgslBackend = { } return primitiveTypeName; }, + getNoiseShaderSnippet() { + return noiseWGSL; + }, + generateHookUniformKey(name, typeInfo) { // For sampler2D types, we don't add them to the uniform struct, // but we still need them in the shader's hooks object so that From a1cd2f9c303506bc5956eda65929ed66e4efa80c Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sun, 5 Apr 2026 00:09:56 +0100 Subject: [PATCH 138/250] Fix typo --- src/math/p5.Vector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index fa3455c707..c9cf317548 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -1034,7 +1034,7 @@ class Vector { * * If only one value is provided, as in `v.div(2)`, then all the components * will be divided by 2. If a value isn't provided for a component, it - * won't change. For example, `v.div(4, 5)` divides `v.x` by, `v.y` by 5, + * won't change. For example, `v.div(4, 5)` divides `v.x` by 4, `v.y` by 5, * and `v.z` by 1. Calling `div()` with no arguments, as in `v.div()`, has * no effect. * From 6039cb14121cd38fb330180fc21d474e1572134b Mon Sep 17 00:00:00 2001 From: Yukti Nandwana Date: Sun, 5 Apr 2026 15:38:49 +0530 Subject: [PATCH 139/250] feat(webgl): add texCoord parameter to getFinalColor hook --- src/webgl/p5.RendererGL.js | 8 ++++---- src/webgl/p5.Shader.js | 2 +- src/webgl/shaders/basic.frag | 5 +++-- src/webgl/shaders/line.frag | 2 +- src/webgl/shaders/normal.frag | 5 +++-- src/webgl/shaders/phong.frag | 2 +- src/webgpu/p5.RendererWebGPU.js | 6 +++--- src/webgpu/shaders/color.js | 2 +- src/webgpu/shaders/line.js | 2 +- src/webgpu/shaders/material.js | 2 +- 10 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index b744baff19..edd4f9969b 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -723,7 +723,7 @@ class RendererGL extends Renderer3D { color.a = components.opacity; return color; }`, - "vec4 getFinalColor": "(vec4 color) { return color; }", + "vec4 getFinalColor": "(vec4 color, vec2 texCoord) { return color; }", "void afterFragment": "() {}", }, } @@ -760,7 +760,7 @@ class RendererGL extends Renderer3D { }, fragment: { "void beforeFragment": "() {}", - "vec4 getFinalColor": "(vec4 color) { return color; }", + "vec4 getFinalColor": "(vec4 color, vec2 texCoord) { return color; }", "void afterFragment": "() {}", }, } @@ -788,7 +788,7 @@ class RendererGL extends Renderer3D { }, fragment: { "void beforeFragment": "() {}", - "vec4 getFinalColor": "(vec4 color) { return color; }", + "vec4 getFinalColor": "(vec4 color, vec2 texCoord) { return color; }", "void afterFragment": "() {}", }, } @@ -820,7 +820,7 @@ class RendererGL extends Renderer3D { fragment: { "void beforeFragment": "() {}", "Inputs getPixelInputs": "(Inputs inputs) { return inputs; }", - "vec4 getFinalColor": "(vec4 color) { return color; }", + "vec4 getFinalColor": "(vec4 color, vec2 texCoord) { return color; }", "bool shouldDiscard": "(bool outside) { return outside; }", "void afterFragment": "() {}", }, diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index a881825948..753c5180bd 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -172,7 +172,7 @@ class Shader { * color.a = components.opacity; * return color; * } - * vec4 getFinalColor(vec4 color) { return color; } + * vec4 getFinalColor(vec4 color, vec2 texCoord) { return color; } * void afterFragment() {} * ``` * diff --git a/src/webgl/shaders/basic.frag b/src/webgl/shaders/basic.frag index 1406964ca9..b7db52b1ad 100644 --- a/src/webgl/shaders/basic.frag +++ b/src/webgl/shaders/basic.frag @@ -1,7 +1,8 @@ IN vec4 vColor; +IN highp vec2 vVertTexCoord; void main(void) { HOOK_beforeFragment(); - OUT_COLOR = HOOK_getFinalColor(vColor); + OUT_COLOR = HOOK_getFinalColor(vColor, vVertTexCoord); OUT_COLOR.rgb *= OUT_COLOR.a; // Premultiply alpha before rendering HOOK_afterFragment(); -} +} \ No newline at end of file diff --git a/src/webgl/shaders/line.frag b/src/webgl/shaders/line.frag index a0ca059040..b1ed298b0d 100644 --- a/src/webgl/shaders/line.frag +++ b/src/webgl/shaders/line.frag @@ -69,6 +69,6 @@ void main() { discard; } } - OUT_COLOR = HOOK_getFinalColor(vec4(inputs.color.rgb, 1.) * inputs.color.a); + OUT_COLOR = HOOK_getFinalColor(vec4(inputs.color.rgb, 1.) * inputs.color.a, vec2(0.0, 0.0)); HOOK_afterFragment(); } diff --git a/src/webgl/shaders/normal.frag b/src/webgl/shaders/normal.frag index 0cb362265a..fbb9258547 100644 --- a/src/webgl/shaders/normal.frag +++ b/src/webgl/shaders/normal.frag @@ -1,6 +1,7 @@ IN vec3 vVertexNormal; +IN highp vec2 vVertTexCoord; void main(void) { HOOK_beforeFragment(); - OUT_COLOR = HOOK_getFinalColor(vec4(vVertexNormal, 1.0)); + OUT_COLOR = HOOK_getFinalColor(vec4(vVertexNormal, 1.0), vVertTexCoord); HOOK_afterFragment(); -} +} \ No newline at end of file diff --git a/src/webgl/shaders/phong.frag b/src/webgl/shaders/phong.frag index 78cfb76163..47ec519d47 100644 --- a/src/webgl/shaders/phong.frag +++ b/src/webgl/shaders/phong.frag @@ -77,7 +77,7 @@ void main(void) { c.ambient = inputs.ambientLight; c.specular = specular; c.emissive = inputs.emissiveMaterial; - OUT_COLOR = HOOK_getFinalColor(HOOK_combineColors(c)); + OUT_COLOR = HOOK_getFinalColor(HOOK_combineColors(c), vTexCoord); OUT_COLOR.rgb *= OUT_COLOR.a; // Premultiply alpha before rendering HOOK_afterFragment(); } diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 2b616e4a63..b52d0dc599 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -2345,7 +2345,7 @@ function rendererWebGPU(p5, fn) { rgb += components.emissive; return vec4(rgb, components.opacity); }`, - "vec4f getFinalColor": "(color: vec4) { return color; }", + "vec4f getFinalColor": "(color: vec4, texCoord: vec2) { return color; }", "void afterFragment": "() {}", }, } @@ -2370,7 +2370,7 @@ function rendererWebGPU(p5, fn) { }, fragment: { "void beforeFragment": "() {}", - "vec4 getFinalColor": "(color: vec4) { return color; }", + "vec4 getFinalColor": "(color: vec4, texCoord: vec2) { return color; }", "void afterFragment": "() {}", }, } @@ -2396,7 +2396,7 @@ function rendererWebGPU(p5, fn) { fragment: { "void beforeFragment": "() {}", "Inputs getPixelInputs": "(inputs: Inputs) { return inputs; }", - "vec4 getFinalColor": "(color: vec4) { return color; }", + "vec4 getFinalColor": "(color: vec4, texCoord: vec2) { return color; }", "bool shouldDiscard": "(outside: bool) { return outside; };", "void afterFragment": "() {}", }, diff --git a/src/webgpu/shaders/color.js b/src/webgpu/shaders/color.js index cea80efc9b..cf4ab00285 100644 --- a/src/webgpu/shaders/color.js +++ b/src/webgpu/shaders/color.js @@ -119,7 +119,7 @@ ${uniforms} @fragment fn main(input: FragmentInput) -> @location(0) vec4 { HOOK_beforeFragment(); - var outColor = HOOK_getFinalColor(input.vColor); + var outColor = HOOK_getFinalColor(input.vColor, input.vVertTexCoord); outColor = vec4(outColor.rgb * outColor.a, outColor.a); HOOK_afterFragment(); return outColor; diff --git a/src/webgpu/shaders/line.js b/src/webgpu/shaders/line.js index a46317cee6..2b58857286 100644 --- a/src/webgpu/shaders/line.js +++ b/src/webgpu/shaders/line.js @@ -362,7 +362,7 @@ fn main(input: StrokeFragmentInput) -> @location(0) vec4 { discard; } } - var col = HOOK_getFinalColor(inputs.color); + var col = HOOK_getFinalColor(inputs.color, vec2(0.0, 0.0)); col = vec4(col.rgb, 1.0) * col.a; HOOK_afterFragment(); return vec4(col); diff --git a/src/webgpu/shaders/material.js b/src/webgpu/shaders/material.js index a740e9d17a..9da60f4628 100644 --- a/src/webgpu/shaders/material.js +++ b/src/webgpu/shaders/material.js @@ -417,7 +417,7 @@ fn main(input: FragmentInput) -> @location(0) vec4 { ); var outColor = HOOK_getFinalColor( - HOOK_combineColors(components) + HOOK_combineColors(components), input.vTexCoord ); outColor = vec4(outColor.rgb * outColor.a, outColor.a); HOOK_afterFragment(); From 31ad84a617f2c6de4cf3fffb3fb98f3a897d09a3 Mon Sep 17 00:00:00 2001 From: Yukti Nandwana Date: Sun, 5 Apr 2026 17:09:45 +0530 Subject: [PATCH 140/250] feat(webgl): add texCoord parameter to getFinalColor hook --- test/unit/visual/cases/webgl.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 7e1889674e..41420ba3cd 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -1134,6 +1134,21 @@ visualSuite('WebGL', function() { screenshot(); }); + visualTest('texCoord is available in getFinalColor', (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const shader = p5.baseColorShader().modify(() => { + getFinalColor((color) => { + color = [texCoord[0], texCoord[1], 0, 1]; + return color; + }); + }); + p5.background(0); + p5.shader(shader); + p5.noStroke(); + p5.plane(50, 50); + screenshot(); +}); + visualSuite('auto-return for shader hooks', () => { visualTest('auto-returns input struct when return is omitted', (p5, screenshot) => { p5.createCanvas(50, 50, p5.WEBGL); From 8e3272c6298f3515f718b468fa08f72b972d6342 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sun, 5 Apr 2026 13:25:27 +0100 Subject: [PATCH 141/250] Guard specific reference to window and documents in presetup hook --- src/core/friendly_errors/fes_core.js | 14 ++++++++------ src/core/friendly_errors/param_validator.js | 2 +- src/core/main.js | 3 +-- src/events/acceleration.js | 7 +++++-- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/core/friendly_errors/fes_core.js b/src/core/friendly_errors/fes_core.js index 8962745918..fdc19f33da 100644 --- a/src/core/friendly_errors/fes_core.js +++ b/src/core/friendly_errors/fes_core.js @@ -26,7 +26,7 @@ import { translator } from '../internationalization'; import errorTable from './browser_errors'; import * as contants from '../constants'; -function fesCore(p5, fn){ +function fesCore(p5, fn, lifecycles){ // p5.js blue, p5.js orange, auto dark green; fallback p5.js darkened magenta // See testColors below for all the color codes and names const typeColors = ['#2D7BB6', '#EE9900', '#4DB200', '#C83C00']; @@ -972,9 +972,11 @@ function fesCore(p5, fn){ p5._fesLogger = null; p5._fesLogCache = {}; - window.addEventListener('load', checkForUserDefinedFunctions, false); - window.addEventListener('error', p5._fesErrorMonitor, false); - window.addEventListener('unhandledrejection', p5._fesErrorMonitor, false); + lifecycles.presetup = function () { + window.addEventListener('load', checkForUserDefinedFunctions, false); + window.addEventListener('error', p5._fesErrorMonitor, false); + window.addEventListener('unhandledrejection', p5._fesErrorMonitor, false); + }; /** * Prints out all the colors in the color pallete with white text. @@ -1134,7 +1136,7 @@ function fesCore(p5, fn){ // Exposing this primarily for unit testing. fn._helpForMisusedAtTopLevelCode = helpForMisusedAtTopLevelCode; - if (document.readyState !== 'complete') { + if (typeof document !== 'undefined' && document.readyState !== 'complete') { window.addEventListener('error', helpForMisusedAtTopLevelCode, false); // Our job is only to catch ReferenceErrors that are thrown when @@ -1150,5 +1152,5 @@ function fesCore(p5, fn){ export default fesCore; if (typeof p5 !== 'undefined') { - fesCore(p5, p5.prototype); + p5.registerAddon(fesCore); } diff --git a/src/core/friendly_errors/param_validator.js b/src/core/friendly_errors/param_validator.js index 5fa4e73151..ddb011a54a 100644 --- a/src/core/friendly_errors/param_validator.js +++ b/src/core/friendly_errors/param_validator.js @@ -618,5 +618,5 @@ function validateParams(p5, fn, lifecycles) { export default validateParams; if (typeof p5 !== 'undefined') { - validateParams(p5, p5.prototype); + p5.registerAddon(validateParams); } diff --git a/src/core/main.js b/src/core/main.js index 00f637d366..a0b501a531 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -50,7 +50,7 @@ class p5 { constructor(sketch, node) { // Apply addon defined decorations if(p5.decorations.size > 0){ - decorateClass(p5, p5.decorations); + decorateClass(p5, p5.decorations, 'p5'); p5.decorations.clear(); } @@ -531,7 +531,6 @@ function createBindGlobal(instance) { // Generic function to decorate classes function decorateClass(Target, decorations, path){ - path ??= Target.name; // Static properties for(const key in Target){ if(!key.startsWith('_')){ diff --git a/src/events/acceleration.js b/src/events/acceleration.js index 7750f6099b..e788171abc 100644 --- a/src/events/acceleration.js +++ b/src/events/acceleration.js @@ -19,6 +19,10 @@ function acceleration(p5, fn, lifecycles){ signal: this._removeSignal }); } + + // Initialize device orientation value + this.deviceOrientation = typeof window !== 'undefined' && + window.innerWidth / window.innerHeight > 1.0 ? 'landscape' : 'portrait'; }; /** @@ -30,8 +34,7 @@ function acceleration(p5, fn, lifecycles){ * @property {(LANDSCAPE|PORTRAIT)} deviceOrientation * @readOnly */ - fn.deviceOrientation = - window.innerWidth / window.innerHeight > 1.0 ? 'landscape' : 'portrait'; + fn.deviceOrientation = 'landscape'; /** * The system variable accelerationX always contains the acceleration of the From 4cd5c2fa53a8fa6b94dc59772d72cae94c039118 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 7 Apr 2026 11:14:00 -0400 Subject: [PATCH 142/250] Fix parsing of control flow in inline uniform callbacks --- src/strands/strands_transpiler.js | 10 ++++ test/unit/webgl/p5.Shader.js | 80 +++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index f6632d7429..098a8bb59e 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -1544,16 +1544,19 @@ function transformSetCallsInControlFlow(ast) { // Collect functions that have .set() calls in control flow const collectFunctions = { ArrowFunctionExpression(node, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } if (functionHasSetInControlFlow(node)) { functionsToTransform.push(node); } }, FunctionExpression(node, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } if (functionHasSetInControlFlow(node)) { functionsToTransform.push(node); } }, FunctionDeclaration(node, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } if (functionHasSetInControlFlow(node)) { functionsToTransform.push(node); } @@ -1575,6 +1578,7 @@ function transformHelperFunctionEarlyReturns(ast) { // Collect helper functions that need transformation const collectHelperFunctions = { VariableDeclarator(node, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } const init = node.init; if (init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) { if (functionHasEarlyReturns(init)) { @@ -1583,6 +1587,7 @@ function transformHelperFunctionEarlyReturns(ast) { } }, FunctionDeclaration(node, ancestors) { + if (ancestors.some(nodeIsUniform)) { return; } if (functionHasEarlyReturns(node)) { helperFunctionsToTransform.push(node); } @@ -1626,6 +1631,11 @@ export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { // Fourth pass: transform if/for statements in post-order using recursive traversal const postOrderControlFlowTransform = { + CallExpression(node, state, c) { + if (nodeIsUniform(node)) { return; } + if (node.callee) c(node.callee, state); + for (const arg of node.arguments) c(arg, state); + }, IfStatement(node, state, c) { state.inControlFlow++; // First recursively process children diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index bdfb0e5476..33e6977e31 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -2184,6 +2184,86 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca assert.approximately(pixelColor[2], 0, 5); }); + test('handle uniformFloat with control flow in callback', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseFilterShader().modify(() => { + // Uniform callback with an if-statement and multiple return paths + const pastOneSecond = myp5.uniformFloat(() => { + if (myp5.frameCount > 1000) { + return 1; + } + return 0; + }); + + myp5.filterColor.begin(); + myp5.filterColor.set(myp5.mix([1, 0, 0, 1], [0, 1, 0, 1], pastOneSecond)); + myp5.filterColor.end(); + }, { myp5 }); + + myp5.background(255, 255, 255); + myp5.filter(testShader); + + // frameCount <= 1000 so pastOneSecond = 0, mix returns [1, 0, 0, 1] = red + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); + assert.approximately(pixelColor[1], 0, 5); + assert.approximately(pixelColor[2], 0, 5); + }); + + test('handle uniformFloat with for loop in callback', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseFilterShader().modify(() => { + // Uniform callback with a for loop accumulating a value + const brightness = myp5.uniformFloat(() => { + let sum = 0; + for (let i = 0; i < 3; i++) { + sum += i; + } + return sum / 10; // 0+1+2=3, 3/10=0.3 + }); + + myp5.filterColor.begin(); + myp5.filterColor.set([brightness, 0, 0, 1]); + myp5.filterColor.end(); + }, { myp5 }); + + myp5.background(255, 255, 255); + myp5.filter(testShader); + + // brightness = 0.3, so red channel = 0.3 * 255 ≈ 76 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 0.3 * 255, 5); + assert.approximately(pixelColor[1], 0, 5); + assert.approximately(pixelColor[2], 0, 5); + }); + + test('handle uniformFloat with sub-function call in callback', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseFilterShader().modify(() => { + // Uniform callback that calls a sub-function + const brightness = myp5.uniformFloat(() => { + const getValue = () => 0.6; + return getValue(); + }); + + myp5.filterColor.begin(); + myp5.filterColor.set([brightness, 0, 0, 1]); + myp5.filterColor.end(); + }, { myp5 }); + + myp5.background(255, 255, 255); + myp5.filter(testShader); + + // brightness = 0.6, so red channel = 0.6 * 255 ≈ 153 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 0.6 * 255, 5); + assert.approximately(pixelColor[1], 0, 5); + assert.approximately(pixelColor[2], 0, 5); + }); + test('handle false .set() in if with content afterwards with flat API', () => { myp5.createCanvas(50, 50, myp5.WEBGL); From de29d3bcb98ee1fd649c4ca4d1397cce0daae356 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 7 Apr 2026 11:42:05 -0400 Subject: [PATCH 143/250] Don't format non-inline uniform callback functions --- src/strands/strands_transpiler.js | 186 ++++++++++++++++++++++-------- test/unit/webgl/p5.Shader.js | 82 +++++++++++++ 2 files changed, 223 insertions(+), 45 deletions(-) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index 098a8bb59e..b30c3380e2 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -40,6 +40,50 @@ function nodeIsUniform(ancestor) { ); } +function nodeIsUniformCallbackFn(node, names) { + if (!names?.size) return false; + if (node.type === 'FunctionDeclaration' && names.has(node.id?.name)) return true; + if ( + node.type === 'VariableDeclarator' && names.has(node.id?.name) && + (node.init?.type === 'FunctionExpression' || node.init?.type === 'ArrowFunctionExpression') + ) { + return true; + } + return false; +} + +function collectUniformCallbackNames(ast) { + // Sub-pass 1: collect all named function definitions + const namedFunctions = new Set(); + ancestor(ast, { + FunctionDeclaration(node) { + if (node.id) namedFunctions.add(node.id.name); + }, + VariableDeclarator(node) { + if ( + node.id?.type === 'Identifier' && + (node.init?.type === 'FunctionExpression' || node.init?.type === 'ArrowFunctionExpression') + ) { + namedFunctions.add(node.id.name); + } + } + }); + // Sub-pass 2: find which of those names are passed as uniform call arguments + const names = new Set(); + ancestor(ast, { + CallExpression(node) { + if (nodeIsUniform(node)) { + for (const arg of node.arguments) { + if (arg.type === 'Identifier' && namedFunctions.has(arg.name)) { + names.add(arg.name); + } + } + } + } + }); + return names; +} + function nodeIsVarying(node) { return node && node.type === 'CallExpression' && ( @@ -192,8 +236,10 @@ function replaceReferences(node, tempVarMap) { } const ASTCallbacks = { - UnaryExpression(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + UnaryExpression(node, state, ancestors) { + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { + return; + } const unaryFnName = UnarySymbolToName[node.operator]; const standardReplacement = (node) => { node.type = 'CallExpression' @@ -236,8 +282,10 @@ const ASTCallbacks = { delete node.argument; delete node.operator; }, - BreakStatement(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + BreakStatement(node, state, ancestors) { + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { + return; + } node.callee = { type: 'Identifier', name: '__p5.break' @@ -245,8 +293,10 @@ const ASTCallbacks = { node.arguments = []; node.type = 'CallExpression'; }, - MemberExpression(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + MemberExpression(node, state, ancestors) { + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { + return; + } // Skip sets -- these will be converted to .set() method // calls at the AssignmentExpression level if ( @@ -272,8 +322,10 @@ const ASTCallbacks = { node.type = 'CallExpression'; } }, - VariableDeclarator(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + VariableDeclarator(node, state, ancestors) { + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { + return; + } if (nodeIsUniform(node.init)) { // Only inject the variable name if the first argument isn't already a string if (node.init.arguments.length === 0 || @@ -298,16 +350,18 @@ const ASTCallbacks = { value: node.id.name } node.init.arguments.unshift(varyingNameLiteral); - _state.varyings[node.id.name] = varyingNameLiteral; + state.varyings[node.id.name] = varyingNameLiteral; } else { // Still track it as a varying even if name wasn't injected - _state.varyings[node.id.name] = node.init.arguments[0]; + state.varyings[node.id.name] = node.init.arguments[0]; } } }, - Identifier(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } - if (_state.varyings[node.name] + Identifier(node, state, ancestors) { + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { + return; + } + if (state.varyings[node.name] && !ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node) ) { node.type = 'CallExpression'; @@ -327,8 +381,10 @@ const ASTCallbacks = { }, // The callbacks for AssignmentExpression and BinaryExpression handle // operator overloading including +=, *= assignment expressions - ArrayExpression(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + ArrayExpression(node, state, ancestors) { + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { + return; + } const original = JSON.parse(JSON.stringify(node)); node.type = 'CallExpression'; node.callee = { @@ -337,8 +393,10 @@ const ASTCallbacks = { }; node.arguments = [original]; }, - AssignmentExpression(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + AssignmentExpression(node, state, ancestors) { + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { + return; + } const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier']; if (node.operator !== '=') { const methodName = replaceBinaryOperator(node.operator.replace('=','')); @@ -367,7 +425,7 @@ const ASTCallbacks = { node.right = rightReplacementNode; } // Handle direct varying variable assignment: myVarying = value - if (_state.varyings[node.left.name]) { + if (state.varyings[node.left.name]) { node.type = 'ExpressionStatement'; node.expression = { type: 'CallExpression', @@ -412,7 +470,7 @@ const ASTCallbacks = { let varyingName = null; // Check if it's a direct identifier: myVarying.xyz - if (node.left.object.type === 'Identifier' && _state.varyings[node.left.object.name]) { + if (node.left.object.type === 'Identifier' && state.varyings[node.left.object.name]) { varyingName = node.left.object.name; } // Check if it's a getValue() call: myVarying.getValue().xyz @@ -420,7 +478,7 @@ const ASTCallbacks = { node.left.object.callee?.type === 'MemberExpression' && node.left.object.callee.property?.name === 'getValue' && node.left.object.callee.object?.type === 'Identifier' && - _state.varyings[node.left.object.callee.object.name]) { + state.varyings[node.left.object.callee.object.name]) { varyingName = node.left.object.callee.object.name; } @@ -451,10 +509,12 @@ const ASTCallbacks = { } } }, - BinaryExpression(node, _state, ancestors) { + BinaryExpression(node, state, ancestors) { // Don't convert uniform default values to node methods, as // they should be evaluated at runtime, not compiled. - if (ancestors.some(nodeIsUniform)) { return; } + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { + return; + } // If the left hand side of an expression is one of these types, // we should construct a node from it. const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier']; @@ -482,10 +542,12 @@ const ASTCallbacks = { }; node.arguments = [node.right]; }, - LogicalExpression(node, _state, ancestors) { + LogicalExpression(node, state, ancestors) { // Don't convert uniform default values to node methods, as // they should be evaluated at runtime, not compiled. - if (ancestors.some(nodeIsUniform)) { return; } + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { + return; + } // If the left hand side of an expression is one of these types, // we should construct a node from it. const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier']; @@ -513,8 +575,10 @@ const ASTCallbacks = { }; node.arguments = [node.right]; }, - ConditionalExpression(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + ConditionalExpression(node, state, ancestors) { + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { + return; + } // Transform condition ? consequent : alternate // into __p5.strandsTernary(condition, consequent, alternate) const test = node.test; @@ -527,8 +591,10 @@ const ASTCallbacks = { delete node.consequent; delete node.alternate; }, - IfStatement(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + IfStatement(node, state, ancestors) { + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { + return; + } // Transform if statement into strandsIf() call // The condition is evaluated directly, not wrapped in a function const condition = node.test; @@ -796,8 +862,10 @@ const ASTCallbacks = { delete node.consequent; delete node.alternate; }, - UpdateExpression(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + UpdateExpression(node, state, ancestors) { + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { + return; + } // Transform ++var, var++, --var, var-- into assignment expressions let operator; @@ -828,11 +896,13 @@ const ASTCallbacks = { // Replace the update expression with the assignment expression Object.assign(node, assignmentExpr); delete node.prefix; - this.BinaryExpression(node.right, _state, [...ancestors, node]); - this.AssignmentExpression(node, _state, ancestors); + this.BinaryExpression(node.right, state, [...ancestors, node]); + this.AssignmentExpression(node, state, ancestors); }, - ForStatement(node, _state, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + ForStatement(node, state, ancestors) { + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { + return; + } // Transform for statement into strandsFor() call // for (init; test; update) body -> strandsFor(initCb, conditionCb, updateCb, bodyCb, initialVars) @@ -1538,25 +1608,31 @@ function transformFunctionSetCalls(functionNode) { } // Main transformation pass: find and transform functions with .set() calls in control flow -function transformSetCallsInControlFlow(ast) { +function transformSetCallsInControlFlow(ast, names) { const functionsToTransform = []; // Collect functions that have .set() calls in control flow const collectFunctions = { ArrowFunctionExpression(node, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, names))) { + return; + } if (functionHasSetInControlFlow(node)) { functionsToTransform.push(node); } }, FunctionExpression(node, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, names))) { + return; + } if (functionHasSetInControlFlow(node)) { functionsToTransform.push(node); } }, FunctionDeclaration(node, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, names))) { + return; + } if (functionHasSetInControlFlow(node)) { functionsToTransform.push(node); } @@ -1572,13 +1648,15 @@ function transformSetCallsInControlFlow(ast) { } // Main transformation pass: find and transform helper functions with early returns -function transformHelperFunctionEarlyReturns(ast) { +function transformHelperFunctionEarlyReturns(ast, names) { const helperFunctionsToTransform = []; // Collect helper functions that need transformation const collectHelperFunctions = { VariableDeclarator(node, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, names))) { + return; + } const init = node.init; if (init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) { if (functionHasEarlyReturns(init)) { @@ -1587,7 +1665,9 @@ function transformHelperFunctionEarlyReturns(ast) { } }, FunctionDeclaration(node, ancestors) { - if (ancestors.some(nodeIsUniform)) { return; } + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, names))) { + return; + } if (functionHasEarlyReturns(node)) { helperFunctionsToTransform.push(node); } @@ -1617,17 +1697,20 @@ export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { locations: srcLocations }); + // Pre-pass: collect names of functions passed by reference as uniform callbacks + const uniformCallbackNames = collectUniformCallbackNames(ast); + // First pass: transform .set() calls in control flow to use intermediate variables - transformSetCallsInControlFlow(ast); + transformSetCallsInControlFlow(ast, uniformCallbackNames); // Second pass: transform everything except if/for statements using normal ancestor traversal const nonControlFlowCallbacks = { ...ASTCallbacks }; delete nonControlFlowCallbacks.IfStatement; delete nonControlFlowCallbacks.ForStatement; - ancestor(ast, nonControlFlowCallbacks, undefined, { varyings: {} }); + ancestor(ast, nonControlFlowCallbacks, undefined, { varyings: {}, uniformCallbackNames }); // Third pass: transform helper functions with early returns to use __returnValue pattern - transformHelperFunctionEarlyReturns(ast); + transformHelperFunctionEarlyReturns(ast, uniformCallbackNames); // Fourth pass: transform if/for statements in post-order using recursive traversal const postOrderControlFlowTransform = { @@ -1636,6 +1719,19 @@ export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { if (node.callee) c(node.callee, state); for (const arg of node.arguments) c(arg, state); }, + FunctionDeclaration(node, state, c) { + if (state.uniformCallbackNames?.has(node.id?.name)) return; + if (node.body) c(node.body, state); + }, + VariableDeclarator(node, state, c) { + if ( + state.uniformCallbackNames?.has(node.id?.name) && + (node.init?.type === 'FunctionExpression' || node.init?.type === 'ArrowFunctionExpression') + ) { + return; + } + if (node.init) c(node.init, state); + }, IfStatement(node, state, c) { state.inControlFlow++; // First recursively process children @@ -1672,7 +1768,7 @@ export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { delete node.argument; } }; - recursive(ast, { varyings: {}, inControlFlow: 0 }, postOrderControlFlowTransform); + recursive(ast, { varyings: {}, inControlFlow: 0, uniformCallbackNames }, postOrderControlFlowTransform); const transpiledSource = escodegen.generate(ast); const scopeKeys = Object.keys(scope); const match = /\(?\s*(?:function)?\s*\w*\s*\(([^)]*)\)\s*(?:=>)?\s*{((?:.|\n)*)}\s*;?\s*\)?/ diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 33e6977e31..24c23fe548 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -2264,6 +2264,88 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca assert.approximately(pixelColor[2], 0, 5); }); + test('handle uniformFloat with control flow in non-inline callback', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseFilterShader().modify(() => { + function pastOneSecondValue() { + if (myp5.frameCount > 1000) { + return 1; + } + return 0; + } + const pastOneSecond = myp5.uniformFloat(pastOneSecondValue); + + myp5.filterColor.begin(); + myp5.filterColor.set(myp5.mix([1, 0, 0, 1], [0, 1, 0, 1], pastOneSecond)); + myp5.filterColor.end(); + }, { myp5 }); + + myp5.background(255, 255, 255); + myp5.filter(testShader); + + // frameCount <= 1000 so pastOneSecond = 0, mix returns [1, 0, 0, 1] = red + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); + assert.approximately(pixelColor[1], 0, 5); + assert.approximately(pixelColor[2], 0, 5); + }); + + test('handle uniformFloat with for loop in non-inline callback', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseFilterShader().modify(() => { + function brightnessValue() { + let sum = 0; + for (let i = 0; i < 3; i++) { + sum += i; + } + return sum / 10; // 0+1+2=3, 3/10=0.3 + } + const brightness = myp5.uniformFloat(brightnessValue); + + myp5.filterColor.begin(); + myp5.filterColor.set([brightness, 0, 0, 1]); + myp5.filterColor.end(); + }, { myp5 }); + + myp5.background(255, 255, 255); + myp5.filter(testShader); + + // brightness = 0.3, so red channel = 0.3 * 255 ≈ 76 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 0.3 * 255, 5); + assert.approximately(pixelColor[1], 0, 5); + assert.approximately(pixelColor[2], 0, 5); + }); + + test('handle uniformFloat with sub-function call in non-inline callback', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseFilterShader().modify(() => { + function getValue() { + return 0.6; + } + function brightnessValue() { + return getValue(); + } + const brightness = myp5.uniformFloat(brightnessValue); + + myp5.filterColor.begin(); + myp5.filterColor.set([brightness, 0, 0, 1]); + myp5.filterColor.end(); + }, { myp5 }); + + myp5.background(255, 255, 255); + myp5.filter(testShader); + + // brightness = 0.6, so red channel = 0.6 * 255 ≈ 153 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 0.6 * 255, 5); + assert.approximately(pixelColor[1], 0, 5); + assert.approximately(pixelColor[2], 0, 5); + }); + test('handle false .set() in if with content afterwards with flat API', () => { myp5.createCanvas(50, 50, myp5.WEBGL); From 1219e71f4b37c8c29301ccca64806c2660bcf3d6 Mon Sep 17 00:00:00 2001 From: Kathrina-dev Date: Tue, 7 Apr 2026 17:53:20 +0000 Subject: [PATCH 144/250] added new typing for objects in p5.strands.js --- src/strands/p5.strands.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index b2ae6e1e61..4a86bfa55c 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -214,7 +214,10 @@ if (typeof p5 !== "undefined") { /* ------------------------------------------------------------- */ /** - * @property {Object} worldInputs + * @typedef {Object} WorldInputsHook +*/ +/** + * @property {WorldInputsHook} worldInputs * @beta * @description * A shader hook block that modifies the world-space properties of each vertex in a shader. This hook can be used inside `buildColorShader()` and similar shader `modify()` calls to customize vertex positions, normals, texture coordinates, and colors before rendering. Modifications happen between the `.begin()` and `.end()` methods of the hook. "World space" refers to the coordinate system of the 3D scene, before any camera or projection transformations are applied. @@ -259,7 +262,10 @@ if (typeof p5 !== "undefined") { */ /** - * @property {Object} combineColors + * @typedef {Object} CombineColorsHook +*/ +/** + * @property {CombineColorsHook} combineColors * @beta * @description * A shader hook block that modifies how color components are combined in the fragment shader. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to control the final color output of a material. Modifications happen between the `.begin()` and `.end()` methods of the hook. @@ -592,7 +598,10 @@ if (typeof p5 !== "undefined") { */ /** - * @property {Object} pixelInputs + * @typedef {Object} PixelInputsHook +*/ +/** + * @property {PixelInputsHook} pixelInputs * @beta * @description * A shader hook block that modifies the properties of each pixel before the final color is calculated. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to adjust per-pixel data before lighting is applied. Modifications happen between the `.begin()` and `.end()` methods of the hook. @@ -818,7 +827,10 @@ if (typeof p5 !== "undefined") { */ /** - * @property {Object} objectInputs + * @typedef {Object} ObjectInputsHook +*/ +/** + * @property {ObjectInputsHook} objectInputs * @beta * @description * A shader hook block to modify the properties of each vertex before any transformations are applied. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to customize vertex positions, normals, texture coordinates, and colors before rendering. Modifications happen between the `.begin()` and `.end()` methods of the hook. "Object space" refers to the coordinate system of the 3D scene before any transformations, cameras, or projection transformations are applied. @@ -860,7 +872,10 @@ if (typeof p5 !== "undefined") { */ /** - * @property {Object} cameraInputs + * @typedef {Object} CameraInputsHook +*/ +/** + * @property {CameraInputsHook} cameraInputs * @beta * @description * A shader hook block that adjusts vertex properties from the perspective of the camera. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to customize vertex positions, normals, texture coordinates, and colors before rendering. "Camera space" refers to the coordinate system of the 3D scene after transformations have been applied, seen relative to the camera. From 59400f663ad1848e6d428b9750537dd909a75368 Mon Sep 17 00:00:00 2001 From: VANSH3104 Date: Tue, 7 Apr 2026 23:42:03 +0530 Subject: [PATCH 145/250] fix: implement dual-path rendering for arcs and add path-safety moveTos --- src/core/p5.Renderer2D.js | 4 +-- src/shape/custom_shapes.js | 53 +++++++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 3a26144218..196e466ce3 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -287,10 +287,10 @@ class Renderer2D extends Renderer { this.clipPath.closePath(); } else { if (this.states.fillColor) { - this.drawingContext.fill(visitor.path); + this.drawingContext.fill(visitor.fillPath || visitor.path); } if (this.states.strokeColor) { - this.drawingContext.stroke(visitor.path); + this.drawingContext.stroke(visitor.strokePath || visitor.path); } } } diff --git a/src/shape/custom_shapes.js b/src/shape/custom_shapes.js index 30862879ab..349b53d283 100644 --- a/src/shape/custom_shapes.js +++ b/src/shape/custom_shapes.js @@ -466,6 +466,14 @@ class Quad extends ShapePrimitive { } } +/** + * TODO: Future enhancement — align with arcVertex proposal (#6459) + * Currently stores start/stop angles and mode (OPEN/CHORD/PIE). + * For full SVG compatibility and arcs inside beginShape/endShape, + * we may want to add an arc-to-vertex variant that matches the + * arcVertex() API discussed in #6459. + */ + class ArcPrimitive extends ShapePrimitive { #x; #y; @@ -1153,6 +1161,8 @@ class PrimitiveVisitor { // requires testing class PrimitiveToPath2DConverter extends PrimitiveVisitor { path = new Path2D(); + strokePath = null; + fillPath = null; strokeWeight; constructor({ strokeWeight }) { @@ -1276,20 +1286,44 @@ class PrimitiveToPath2DConverter extends PrimitiveVisitor { const centerY = arc.y + arc.h / 2; const radiusX = arc.w / 2; const radiusY = arc.h / 2; + const startX = centerX + radiusX * Math.cos(arc.start); + const startY = centerY + radiusY * Math.sin(arc.start); - this.path.ellipse( - centerX, centerY, radiusX, radiusY, 0, arc.start, arc.stop + const delta = arc.stop - arc.start; + const isFullCircle = Math.abs(delta % (2 * Math.PI)) < 0.00001 && + Math.abs(delta) > 0.00001; + + const createPieSlice = ! ( + arc.mode === constants.CHORD || + arc.mode === constants.OPEN || + isFullCircle ); - if (arc.mode === constants.OPEN) { - // OPEN: leave path open — arc stroke/fill is just the curve - } else if (arc.mode === constants.CHORD) { + if (!this.fillPath) this.fillPath = new Path2D(this.path); + if (!this.strokePath) this.strokePath = new Path2D(this.path); - this.path.closePath(); - } else { - this.path.lineTo(centerX, centerY); - this.path.closePath(); + this.fillPath.moveTo(startX, startY); + this.fillPath.ellipse(centerX, centerY, radiusX, radiusY, + 0, arc.start, arc.stop); + if (createPieSlice) { + this.fillPath.lineTo(centerX, centerY); + } + this.fillPath.closePath(); + + this.strokePath.moveTo(startX, startY); + this.strokePath.ellipse(centerX, centerY, radiusX, radiusY, + 0, arc.start, arc.stop); + if (arc.mode === constants.PIE && createPieSlice) { + this.strokePath.lineTo(centerX, centerY); } + if (arc.mode === constants.PIE || arc.mode === constants.CHORD) { + this.strokePath.closePath(); + } + + // Still maintain base path just in case + this.path.moveTo(startX, startY); + this.path.ellipse(centerX, centerY, radiusX, radiusY, + 0, arc.start, arc.stop); } visitEllipsePrimitive(ellipse) { const centerX = ellipse.x + ellipse.w / 2; @@ -1297,6 +1331,7 @@ class PrimitiveToPath2DConverter extends PrimitiveVisitor { const radiusX = ellipse.w / 2; const radiusY = ellipse.h / 2; + this.path.moveTo(centerX + radiusX, centerY); this.path.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); } visitQuadStrip(quadStrip) { From 9db7e5e33648f07e720c5b696dc2ac67bf9c491c Mon Sep 17 00:00:00 2001 From: nbogie Date: Wed, 8 Apr 2026 13:04:54 +0100 Subject: [PATCH 146/250] remove unused @requires jsdoc tag from src/ --- src/accessibility/color_namer.js | 1 - src/accessibility/describe.js | 1 - src/accessibility/gridOutput.js | 1 - src/accessibility/outputs.js | 1 - src/accessibility/textOutput.js | 1 - src/color/color_conversion.js | 1 - src/color/creating_reading.js | 2 -- src/color/p5.Color.js | 2 -- src/color/setting.js | 2 -- src/core/environment.js | 2 -- src/core/friendly_errors/fes_core.js | 1 - src/core/friendly_errors/file_errors.js | 1 - src/core/friendly_errors/param_validator.js | 1 - src/core/friendly_errors/stacktrace.js | 1 - src/core/helpers.js | 1 - src/core/legacy.js | 1 - src/core/main.js | 1 - src/core/structure.js | 1 - src/core/transform.js | 2 -- src/data/local_storage.js | 1 - src/dom/dom.js | 1 - src/events/acceleration.js | 1 - src/events/keyboard.js | 1 - src/events/pointer.js | 2 -- src/image/image.js | 1 - src/image/loading_displaying.js | 1 - src/image/p5.Image.js | 3 --- src/image/pixels.js | 1 - src/io/files.js | 1 - src/io/p5.Table.js | 1 - src/io/p5.TableRow.js | 1 - src/io/p5.XML.js | 1 - src/math/Matrices/MatrixNumjs.js | 1 - src/math/calculation.js | 1 - src/math/math.js | 1 - src/math/noise.js | 1 - src/math/p5.Matrix.js | 1 - src/math/p5.Vector.js | 1 - src/math/random.js | 1 - src/math/trigonometry.js | 2 -- src/shape/2d_primitives.js | 2 -- src/shape/attributes.js | 2 -- src/shape/curves.js | 1 - src/shape/custom_shapes.js | 2 -- src/shape/vertex.js | 2 -- src/strands/p5.strands.js | 1 - src/type/textCore.js | 1 - src/utilities/conversion.js | 1 - src/utilities/time_date.js | 1 - src/utilities/utility_functions.js | 1 - src/webgl/3d_primitives.js | 2 -- src/webgl/interaction.js | 1 - src/webgl/light.js | 1 - src/webgl/loading.js | 2 -- src/webgl/material.js | 1 - src/webgl/p5.Camera.js | 1 - src/webgl/p5.Framebuffer.js | 1 - src/webgl/p5.Geometry.js | 2 -- src/webgl/p5.Shader.js | 1 - src/webgl/p5.Texture.js | 1 - 60 files changed, 76 deletions(-) diff --git a/src/accessibility/color_namer.js b/src/accessibility/color_namer.js index bfe1c96a58..9a9225c2eb 100644 --- a/src/accessibility/color_namer.js +++ b/src/accessibility/color_namer.js @@ -2,7 +2,6 @@ * @module Environment * @submodule Environment * @for p5 - * @requires core */ import color_conversion from '../color/color_conversion'; diff --git a/src/accessibility/describe.js b/src/accessibility/describe.js index 4ede8cef65..8310fda3bf 100644 --- a/src/accessibility/describe.js +++ b/src/accessibility/describe.js @@ -2,7 +2,6 @@ * @module Environment * @submodule Environment * @for p5 - * @requires core */ function describe(p5, fn){ diff --git a/src/accessibility/gridOutput.js b/src/accessibility/gridOutput.js index 21b900b24e..fc3e67ed78 100644 --- a/src/accessibility/gridOutput.js +++ b/src/accessibility/gridOutput.js @@ -2,7 +2,6 @@ * @module Environment * @submodule Environment * @for p5 - * @requires core */ function gridOutput(p5, fn){ diff --git a/src/accessibility/outputs.js b/src/accessibility/outputs.js index 1443d9004c..5fd5183016 100644 --- a/src/accessibility/outputs.js +++ b/src/accessibility/outputs.js @@ -2,7 +2,6 @@ * @module Environment * @submodule Environment * @for p5 - * @requires core */ function outputs(p5, fn){ diff --git a/src/accessibility/textOutput.js b/src/accessibility/textOutput.js index 971718fc26..46baa56071 100644 --- a/src/accessibility/textOutput.js +++ b/src/accessibility/textOutput.js @@ -2,7 +2,6 @@ * @module Environment * @submodule Environment * @for p5 - * @requires core */ function textOutput(p5, fn){ diff --git a/src/color/color_conversion.js b/src/color/color_conversion.js index 850d24918b..11823f077e 100644 --- a/src/color/color_conversion.js +++ b/src/color/color_conversion.js @@ -2,7 +2,6 @@ * @module Color * @submodule Color Conversion * @for p5 - * @requires core */ /** diff --git a/src/color/creating_reading.js b/src/color/creating_reading.js index a6aa934dc1..7690104406 100644 --- a/src/color/creating_reading.js +++ b/src/color/creating_reading.js @@ -2,8 +2,6 @@ * @module Color * @submodule Creating & Reading * @for p5 - * @requires core - * @requires constants */ import { Color } from './p5.Color'; diff --git a/src/color/p5.Color.js b/src/color/p5.Color.js index e3724bf4ff..29b5996fb3 100644 --- a/src/color/p5.Color.js +++ b/src/color/p5.Color.js @@ -2,8 +2,6 @@ * @module Color * @submodule Creating & Reading * @for p5 - * @requires core - * @requires color_conversion */ import { RGB, RGBHDR, HSL, HSB, HWB, LAB, LCH, OKLAB, OKLCH } from './creating_reading'; diff --git a/src/color/setting.js b/src/color/setting.js index a8e8d0b1b3..04ddb8372f 100644 --- a/src/color/setting.js +++ b/src/color/setting.js @@ -2,8 +2,6 @@ * @module Color * @submodule Setting * @for p5 - * @requires core - * @requires constants */ import * as constants from '../core/constants'; diff --git a/src/core/environment.js b/src/core/environment.js index 6fffe81973..c27683cf31 100644 --- a/src/core/environment.js +++ b/src/core/environment.js @@ -2,8 +2,6 @@ * @module Environment * @submodule Environment * @for p5 - * @requires core - * @requires constants */ import * as C from './constants'; diff --git a/src/core/friendly_errors/fes_core.js b/src/core/friendly_errors/fes_core.js index 8962745918..f86389e12c 100644 --- a/src/core/friendly_errors/fes_core.js +++ b/src/core/friendly_errors/fes_core.js @@ -1,6 +1,5 @@ /** * @for p5 - * @requires core * * This is the main file for the Friendly Error System (FES), containing * the core as well as miscellaneous functionality of the FES. Here is a diff --git a/src/core/friendly_errors/file_errors.js b/src/core/friendly_errors/file_errors.js index 8f212c8355..75b8462b72 100644 --- a/src/core/friendly_errors/file_errors.js +++ b/src/core/friendly_errors/file_errors.js @@ -1,6 +1,5 @@ /** * @for p5 - * @requires core */ import { translator } from '../internationalization'; diff --git a/src/core/friendly_errors/param_validator.js b/src/core/friendly_errors/param_validator.js index 5fa4e73151..4e9f912773 100644 --- a/src/core/friendly_errors/param_validator.js +++ b/src/core/friendly_errors/param_validator.js @@ -1,6 +1,5 @@ /** * @for p5 - * @requires core */ import * as constants from '../constants.js'; import { z } from 'zod/v4'; diff --git a/src/core/friendly_errors/stacktrace.js b/src/core/friendly_errors/stacktrace.js index 1777abac41..1c60713f9f 100644 --- a/src/core/friendly_errors/stacktrace.js +++ b/src/core/friendly_errors/stacktrace.js @@ -1,6 +1,5 @@ /** * @for p5 - * @requires core */ // Borrow from stacktracejs https://github.com/stacktracejs/stacktrace.js with // minor modifications. The license for the same and the code is included below diff --git a/src/core/helpers.js b/src/core/helpers.js index 0bc0ddbe3d..9f31e78d8a 100644 --- a/src/core/helpers.js +++ b/src/core/helpers.js @@ -1,5 +1,4 @@ /** - * @requires constants */ import * as constants from './constants'; diff --git a/src/core/legacy.js b/src/core/legacy.js index bf05901333..6253b8894e 100644 --- a/src/core/legacy.js +++ b/src/core/legacy.js @@ -1,6 +1,5 @@ /** * @for p5 - * @requires core * These are functions that are part of the Processing API but are not part of * the p5.js API. In some cases they have a new name, in others, they are * removed completely. Not all unsupported Processing functions are listed here diff --git a/src/core/main.js b/src/core/main.js index 4ce9d91c55..7f8af440a2 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -2,7 +2,6 @@ * @module Structure * @submodule Structure * @for p5 - * @requires constants */ import * as constants from './constants'; diff --git a/src/core/structure.js b/src/core/structure.js index 19c96c2156..6d17882f27 100644 --- a/src/core/structure.js +++ b/src/core/structure.js @@ -2,7 +2,6 @@ * @module Structure * @submodule Structure * @for p5 - * @requires core */ function structure(p5, fn){ diff --git a/src/core/transform.js b/src/core/transform.js index 0b8eb95a5b..4ff359d5a9 100644 --- a/src/core/transform.js +++ b/src/core/transform.js @@ -2,8 +2,6 @@ * @module Transform * @submodule Transform * @for p5 - * @requires core - * @requires constants */ function transform(p5, fn){ diff --git a/src/data/local_storage.js b/src/data/local_storage.js index f625618c95..505b500d61 100644 --- a/src/data/local_storage.js +++ b/src/data/local_storage.js @@ -1,7 +1,6 @@ /** * @module Data * @submodule LocalStorage - * @requires core * * This module defines the p5 methods for working with local storage */ diff --git a/src/dom/dom.js b/src/dom/dom.js index 9f1dcb14c7..e975740ca0 100644 --- a/src/dom/dom.js +++ b/src/dom/dom.js @@ -13,7 +13,6 @@ * @module DOM * @submodule DOM * @for p5 - * @requires p5 */ import { Element } from './p5.Element'; diff --git a/src/events/acceleration.js b/src/events/acceleration.js index 7750f6099b..0d1557d5c1 100644 --- a/src/events/acceleration.js +++ b/src/events/acceleration.js @@ -2,7 +2,6 @@ * @module Events * @submodule Acceleration * @for p5 - * @requires core * @main Events */ diff --git a/src/events/keyboard.js b/src/events/keyboard.js index eee2195f4d..0e51b0632c 100644 --- a/src/events/keyboard.js +++ b/src/events/keyboard.js @@ -2,7 +2,6 @@ * @module Events * @submodule Keyboard * @for p5 - * @requires core */ export function isCode(input) { const leftRightKeys = [ diff --git a/src/events/pointer.js b/src/events/pointer.js index b14b7c25d4..3fc8560302 100644 --- a/src/events/pointer.js +++ b/src/events/pointer.js @@ -2,8 +2,6 @@ * @module Events * @submodule Pointer * @for p5 - * @requires core - * @requires constants */ function pointer(p5, fn, lifecycles){ diff --git a/src/image/image.js b/src/image/image.js index 707892fe42..2296f5a414 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -2,7 +2,6 @@ * @module Image * @submodule Image * @for p5 - * @requires core */ /** diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 28149c4412..81f18b9a64 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -2,7 +2,6 @@ * @module Image * @submodule Loading & Displaying * @for p5 - * @requires core */ import canvas from '../core/helpers'; diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js index 80e5781524..431d618e77 100644 --- a/src/image/p5.Image.js +++ b/src/image/p5.Image.js @@ -1,9 +1,6 @@ /** * @module Image * @submodule Image - * @requires core - * @requires constants - * @requires filters */ /** diff --git a/src/image/pixels.js b/src/image/pixels.js index c1b7b4abe6..50e315166a 100644 --- a/src/image/pixels.js +++ b/src/image/pixels.js @@ -2,7 +2,6 @@ * @module Image * @submodule Pixels * @for p5 - * @requires core */ import Filters from './filters'; diff --git a/src/io/files.js b/src/io/files.js index 41af75877e..013f4803d5 100644 --- a/src/io/files.js +++ b/src/io/files.js @@ -2,7 +2,6 @@ * @module IO * @submodule Input * @for p5 - * @requires core */ import { Renderer } from '../core/p5.Renderer'; diff --git a/src/io/p5.Table.js b/src/io/p5.Table.js index 691742a462..47f5ccf273 100644 --- a/src/io/p5.Table.js +++ b/src/io/p5.Table.js @@ -1,7 +1,6 @@ /** * @module IO * @submodule Table - * @requires core */ import { stringify } from './csv'; diff --git a/src/io/p5.TableRow.js b/src/io/p5.TableRow.js index 03e59936ad..59e5d028ac 100644 --- a/src/io/p5.TableRow.js +++ b/src/io/p5.TableRow.js @@ -1,7 +1,6 @@ /** * @module IO * @submodule Table - * @requires core */ class TableRow { diff --git a/src/io/p5.XML.js b/src/io/p5.XML.js index e4e010f839..ba1cba3b9c 100644 --- a/src/io/p5.XML.js +++ b/src/io/p5.XML.js @@ -1,7 +1,6 @@ /** * @module IO * @submodule Input - * @requires core */ class XML { diff --git a/src/math/Matrices/MatrixNumjs.js b/src/math/Matrices/MatrixNumjs.js index 8ee35a7ee8..22ccf2e875 100644 --- a/src/math/Matrices/MatrixNumjs.js +++ b/src/math/Matrices/MatrixNumjs.js @@ -3,7 +3,6 @@ import { Vector } from '../p5.Vector'; import { MatrixInterface } from './MatrixInterface'; /** - * @requires constants * @todo see methods below needing further implementation. * future consideration: implement SIMD optimizations * when browser compatibility becomes available diff --git a/src/math/calculation.js b/src/math/calculation.js index a97e549854..82b788e788 100644 --- a/src/math/calculation.js +++ b/src/math/calculation.js @@ -2,7 +2,6 @@ * @module Math * @submodule Calculation * @for p5 - * @requires core */ function calculation(p5, fn){ diff --git a/src/math/math.js b/src/math/math.js index 9e513a7c36..1c4a7e0fa9 100644 --- a/src/math/math.js +++ b/src/math/math.js @@ -1,7 +1,6 @@ /** * @module Math * @for p5 - * @requires core */ function math(p5, fn) { diff --git a/src/math/noise.js b/src/math/noise.js index 0105615fc8..16c08c484c 100644 --- a/src/math/noise.js +++ b/src/math/noise.js @@ -15,7 +15,6 @@ * @module Math * @submodule Noise * @for p5 - * @requires core */ function noise(p5, fn){ const PERLIN_YWRAPB = 4; diff --git a/src/math/p5.Matrix.js b/src/math/p5.Matrix.js index b6488f76d0..8923bb420c 100644 --- a/src/math/p5.Matrix.js +++ b/src/math/p5.Matrix.js @@ -1,6 +1,5 @@ /** * @module Math - * @requires constants * @todo see methods below needing further implementation. * future consideration: implement SIMD optimizations * when browser compatibility becomes available diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index c9cf317548..2f9682ad39 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -1,6 +1,5 @@ /** * @module Math - * @requires constants */ import * as constants from '../core/constants'; diff --git a/src/math/random.js b/src/math/random.js index 984094fc3d..cd56ed5530 100644 --- a/src/math/random.js +++ b/src/math/random.js @@ -2,7 +2,6 @@ * @module Math * @submodule Random * @for p5 - * @requires core */ function random(p5, fn){ diff --git a/src/math/trigonometry.js b/src/math/trigonometry.js index d2c5491b7d..0ca07bf84a 100644 --- a/src/math/trigonometry.js +++ b/src/math/trigonometry.js @@ -2,8 +2,6 @@ * @module Math * @submodule Trigonometry * @for p5 - * @requires core - * @requires constants */ import * as constants from '../core/constants'; diff --git a/src/shape/2d_primitives.js b/src/shape/2d_primitives.js index aa246664b8..d5a57f1cb8 100644 --- a/src/shape/2d_primitives.js +++ b/src/shape/2d_primitives.js @@ -2,8 +2,6 @@ * @module Shape * @submodule 2D Primitives * @for p5 - * @requires core - * @requires constants */ import * as constants from '../core/constants'; diff --git a/src/shape/attributes.js b/src/shape/attributes.js index 812159267f..ab5ec21a01 100644 --- a/src/shape/attributes.js +++ b/src/shape/attributes.js @@ -2,8 +2,6 @@ * @module Shape * @submodule Attributes * @for p5 - * @requires core - * @requires constants */ import * as constants from '../core/constants'; diff --git a/src/shape/curves.js b/src/shape/curves.js index 5f5e278811..7e2b49a2a1 100644 --- a/src/shape/curves.js +++ b/src/shape/curves.js @@ -2,7 +2,6 @@ * @module Shape * @submodule Curves * @for p5 - * @requires core */ function curves(p5, fn){ diff --git a/src/shape/custom_shapes.js b/src/shape/custom_shapes.js index f287a4823e..0840d3d357 100644 --- a/src/shape/custom_shapes.js +++ b/src/shape/custom_shapes.js @@ -2,8 +2,6 @@ * @module Shape * @submodule Custom Shapes * @for p5 - * @requires core - * @requires constants */ // REMINDER: remove .js extension (currently using it to run file locally) diff --git a/src/shape/vertex.js b/src/shape/vertex.js index fbe46e0cba..0f41a6c043 100644 --- a/src/shape/vertex.js +++ b/src/shape/vertex.js @@ -2,8 +2,6 @@ * @module Shape * @submodule Custom Shapes * @for p5 - * @requires core - * @requires constants */ function vertex(p5, fn){ diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index d8c839847e..8360dd2468 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -2,7 +2,6 @@ * @module 3D * @submodule p5.strands * @for p5 - * @requires core */ import { transpileStrandsToJS } from "./strands_transpiler"; diff --git a/src/type/textCore.js b/src/type/textCore.js index 909609ef0c..079294b64a 100644 --- a/src/type/textCore.js +++ b/src/type/textCore.js @@ -1,6 +1,5 @@ /** * @module Typography - * @requires core */ import { Renderer } from '../core/p5.Renderer'; diff --git a/src/utilities/conversion.js b/src/utilities/conversion.js index 7ebe26ba8f..5727735156 100644 --- a/src/utilities/conversion.js +++ b/src/utilities/conversion.js @@ -2,7 +2,6 @@ * @module Data * @submodule Conversion * @for p5 - * @requires core */ function conversion(p5, fn){ diff --git a/src/utilities/time_date.js b/src/utilities/time_date.js index ab240ee058..029b36a7cc 100644 --- a/src/utilities/time_date.js +++ b/src/utilities/time_date.js @@ -2,7 +2,6 @@ * @module IO * @submodule Time & Date * @for p5 - * @requires core */ function timeDate(p5, fn){ diff --git a/src/utilities/utility_functions.js b/src/utilities/utility_functions.js index 4929522642..11964db774 100644 --- a/src/utilities/utility_functions.js +++ b/src/utilities/utility_functions.js @@ -2,7 +2,6 @@ * @module Data * @submodule Utility Functions * @for p5 - * @requires core */ function utilityFunctions(p5, fn){ diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index cd6cccc893..5eb78db6c6 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -2,8 +2,6 @@ * @module Shape * @submodule 3D Primitives * @for p5 - * @requires core - * @requires p5.Geometry */ import * as constants from '../core/constants'; diff --git a/src/webgl/interaction.js b/src/webgl/interaction.js index ef5beb665a..e65f3a2729 100644 --- a/src/webgl/interaction.js +++ b/src/webgl/interaction.js @@ -2,7 +2,6 @@ * @module 3D * @submodule Interaction * @for p5 - * @requires core */ import * as constants from '../core/constants'; diff --git a/src/webgl/light.js b/src/webgl/light.js index 72938bc291..483b9a53b7 100644 --- a/src/webgl/light.js +++ b/src/webgl/light.js @@ -2,7 +2,6 @@ * @module 3D * @submodule Lights * @for p5 - * @requires core */ import { Renderer3D } from '../core/p5.Renderer3D'; diff --git a/src/webgl/loading.js b/src/webgl/loading.js index e1d726389c..51eef5b09d 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -2,8 +2,6 @@ * @module Shape * @submodule 3D Models * @for p5 - * @requires core - * @requires p5.Geometry */ import { Geometry } from './p5.Geometry'; diff --git a/src/webgl/material.js b/src/webgl/material.js index 624f9eaaf3..000a12fb7c 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -2,7 +2,6 @@ * @module 3D * @submodule Material * @for p5 - * @requires core */ import * as constants from "../core/constants"; diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js index 5dcca97a20..01fa642fc2 100644 --- a/src/webgl/p5.Camera.js +++ b/src/webgl/p5.Camera.js @@ -1,7 +1,6 @@ /** * @module 3D * @submodule Camera - * @requires core */ import { Matrix } from '../math/p5.Matrix'; diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index 29ef58bc3f..fc04f83792 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -1,6 +1,5 @@ /** * @module Rendering - * @requires constants */ import * as constants from '../core/constants'; diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index a2c8992885..2c53833004 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -2,8 +2,6 @@ * @module Shape * @submodule 3D Primitives * @for p5 - * @requires core - * @requires p5.Geometry */ //some of the functions are adjusted from Three.js(http://threejs.org) diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index a881825948..82a4f603ec 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -3,7 +3,6 @@ * @module 3D * @submodule Material * @for p5 - * @requires core */ const TypedArray = Object.getPrototypeOf(Uint8Array); diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index 7441ae5139..3c12e6ed69 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -3,7 +3,6 @@ * @module 3D * @submodule Material * @for p5 - * @requires core */ import * as constants from '../core/constants'; From a16e552284c078d8e2bd5afdfa20c2a0cee0ceee Mon Sep 17 00:00:00 2001 From: nbogie Date: Wed, 8 Apr 2026 13:06:34 +0100 Subject: [PATCH 147/250] remove doc of unused @requires tag from ref guide --- .../contributing_to_the_p5js_reference.md | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/contributor_docs/contributing_to_the_p5js_reference.md b/contributor_docs/contributing_to_the_p5js_reference.md index 0bd05a209d..e4399211a6 100644 --- a/contributor_docs/contributing_to_the_p5js_reference.md +++ b/contributor_docs/contributing_to_the_p5js_reference.md @@ -977,21 +977,6 @@ Example: */ ``` -#### The @requires tag - -The `@requires` tag defines the required imported modules that the current module depends on. - -Example of `@for` and `@requires` - -```js -/** - * @module Color - * @submodule Creating & Reading - * @for p5 - * @requires core - * @requires constants - */ -``` #### The @beta tag - marking experimental API features This tag is used to mark that a feature is experimental and that its details may change or it may be removed. A warning will be presented explaining this on the reference page. From efd82175e8a12ab97e157a6f6c039e3c90acb3fd Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Fri, 10 Apr 2026 06:08:35 +0530 Subject: [PATCH 148/250] Simplify auto-spreading to use 2D square for all cases based on benchmark results --- src/webgpu/p5.RendererWebGPU.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 05f7167a91..8f841db519 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -3827,21 +3827,16 @@ ${hookUniformFields}} const isLarge1D = totalIterations > 1024 && y === 1 && z === 1; if (exceedsLimits || isLarge1D) { - if (totalIterations > 1000000) { - // 3D cube type for extreme large counts - px = Math.ceil(Math.pow(totalIterations, 1 / 3)); - py = Math.ceil(Math.pow(totalIterations, 1 / 3)); - pz = Math.ceil(totalIterations / (px * py)); - } else { - // 2D square type for moderate large counts - px = Math.ceil(Math.sqrt(totalIterations)); - py = Math.ceil(totalIterations / px); - pz = 1; - } + // Always use 2D square spreading (√N × √N). + // Benchmarks showed 2D square equals or outperforms 3D cube at every + // scale tested, with simpler index reconstruction in the shader. + px = Math.ceil(Math.sqrt(totalIterations)); + py = Math.ceil(totalIterations / px); + pz = 1; if (p5.debug || exceedsLimits) { console.warn( - `p5.js: Compute dispatch (${x}, ${y}, ${z}) auto-spread to (${px}, ${py}, ${pz}) ` + + `p5.js: Compute dispatch (${x}, ${y}, ${z}) auto-spread to (${px}, ${py}, 1) ` + `to ${exceedsLimits ? 'stay within GPU limits' : 'optimize performance'}.` ); } From bde2d1e1bc2c2210528389c95bd4cf6d28d5720d Mon Sep 17 00:00:00 2001 From: Shreya Sharma Date: Fri, 10 Apr 2026 13:34:32 +0530 Subject: [PATCH 149/250] Added tests for min and max with Infinity or -Infinity --- test/unit/math/calculation.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/unit/math/calculation.js b/test/unit/math/calculation.js index 1e0e62c1d0..82868210b8 100644 --- a/test/unit/math/calculation.js +++ b/test/unit/math/calculation.js @@ -299,6 +299,14 @@ suite('Calculation', function() { result = mockP5Prototype.max([10, 10]); assert.equal(result, 10); }); + test('should handle Infinity as a valid argument', function() { + result = mockP5Prototype.max(Infinity, 42); + assert.equal(result, Infinity); + }); + test('should handle -Infinity as a valid argument', function() { + result = mockP5Prototype.max(-Infinity, 42); + assert.equal(result, 42); + }); }); suite('p5.prototype.min', function() { @@ -331,6 +339,14 @@ suite('Calculation', function() { result = mockP5Prototype.min([10, 10]); assert.equal(result, 10); }); + test('should handle Infinity as a valid argument', function() { + result = mockP5Prototype.min(Infinity, 42); + assert.equal(result, 42); + }); + test('should handle -Infinity as a valid argument', function() { + result = mockP5Prototype.min(-Infinity, 42); + assert.equal(result, -Infinity); + }); }); suite('p5.prototype.norm', function() { From cd2e29fc0560ec92197ae3ebc43ec03c2101340e Mon Sep 17 00:00:00 2001 From: Yukti Nandwana Date: Fri, 10 Apr 2026 21:22:46 +0530 Subject: [PATCH 150/250] test: update hook signatures in framebuffer and missing frag files --- src/webgpu/shaders/material.js | 6 ++--- test/unit/visual/cases/webgl.js | 23 +++++++++--------- .../000.png | Bin 0 -> 390 bytes .../metadata.json | 3 +++ test/unit/webgl/p5.Shader.js | 2 +- 5 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/texCoord is available in getFinalColor/000.png create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/texCoord is available in getFinalColor/metadata.json diff --git a/src/webgpu/shaders/material.js b/src/webgpu/shaders/material.js index 9da60f4628..751ec0ad3a 100644 --- a/src/webgpu/shaders/material.js +++ b/src/webgpu/shaders/material.js @@ -416,9 +416,9 @@ fn main(input: FragmentInput) -> @location(0) vec4 { inputs.emissiveMaterial ); - var outColor = HOOK_getFinalColor( - HOOK_combineColors(components), input.vTexCoord - ); + var outColor = HOOK_getFinalColor( + HOOK_combineColors(components), input.vTexCoord + ); outColor = vec4(outColor.rgb * outColor.a, outColor.a); HOOK_afterFragment(); return outColor; diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 41420ba3cd..0941c769cf 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -1135,19 +1135,18 @@ visualSuite('WebGL', function() { }); visualTest('texCoord is available in getFinalColor', (p5, screenshot) => { - p5.createCanvas(50, 50, p5.WEBGL); - const shader = p5.baseColorShader().modify(() => { - getFinalColor((color) => { - color = [texCoord[0], texCoord[1], 0, 1]; - return color; + p5.createCanvas(50, 50, p5.WEBGL); + const shader = p5.baseColorShader().modify(() => { + p5.finalColor.begin(); + p5.finalColor.set([p5.finalColor.texCoord, 0, 1]); + p5.finalColor.end(); + }, { p5 }); + p5.background(0); + p5.shader(shader); + p5.noStroke(); + p5.plane(50, 50); + screenshot(); }); - }); - p5.background(0); - p5.shader(shader); - p5.noStroke(); - p5.plane(50, 50); - screenshot(); -}); visualSuite('auto-return for shader hooks', () => { visualTest('auto-returns input struct when return is omitted', (p5, screenshot) => { diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/texCoord is available in getFinalColor/000.png b/test/unit/visual/screenshots/WebGL/p5.strands/texCoord is available in getFinalColor/000.png new file mode 100644 index 0000000000000000000000000000000000000000..460f144e680fdec5c424ba6a89b6d18b8220d1f3 GIT binary patch literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2o;fFj{-MIEGX(zPY*3*fmh#_{X`Q z+X6*gfoLL_3>5Li$~8Rv{SR~gu{XDOT3he`;F*7J-|pYm#pm}H-_A(cZ#?6u%(IOR zt~!a5+7U;ku5E1e%}JEDzHwCUTVaE<%tHxvi=&d~3LCv+9!i<-I4b+@V}rZRLrMLL zqtf?2Hu~2*l(x?>OAT{eeZl3uWA7KultnH^OE0c>?F2G|G$v*mYcEK>B{wfSPF&M+_BY5C27^XSzbqs_rln%LYg1;-J9>nrMl=m z*R)r=y*}U3G+G;~JoD0v>x*1|i5O{4@i;g6@BR1xTZJy3%(J?w{H{!Q(F+07t0x44 z<-gDOO1V?g_Tw!-1H=FS>vsRD0fr_UC>WU;VlUMMu|JQ_1&T0uy85}Sb4q9e0JnOj A;s5{u literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/texCoord is available in getFinalColor/metadata.json b/test/unit/visual/screenshots/WebGL/p5.strands/texCoord is available in getFinalColor/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/p5.strands/texCoord is available in getFinalColor/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 24c23fe548..1e307f51cc 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -318,7 +318,7 @@ suite('p5.Shader', function() { uniforms: { 'sampler2D myTex': null }, - 'vec4 getFinalColor': `(vec4 c) { + 'vec4 getFinalColor': `(vec4 c, vec2 texCoord) { return getTexture(myTex, vec2(0.,0.)); }` }); From db7cd99a0662a11b10dce3c325d97d8c025ccb7e Mon Sep 17 00:00:00 2001 From: VANSH3104 Date: Fri, 10 Apr 2026 23:11:57 +0530 Subject: [PATCH 151/250] fix: remove extra * that make test fail --- src/shape/custom_shapes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shape/custom_shapes.js b/src/shape/custom_shapes.js index 349b53d283..ec9c9530a1 100644 --- a/src/shape/custom_shapes.js +++ b/src/shape/custom_shapes.js @@ -466,7 +466,7 @@ class Quad extends ShapePrimitive { } } -/** +/* * TODO: Future enhancement — align with arcVertex proposal (#6459) * Currently stores start/stop angles and mode (OPEN/CHORD/PIE). * For full SVG compatibility and arcs inside beginShape/endShape, From 01517dbc319d83e1337fbb137e45eb2f683a358a Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 10 Apr 2026 18:52:07 -0400 Subject: [PATCH 152/250] Pass p5/FES into Vector and decorators --- src/core/main.js | 24 ++++++++++++++-------- src/image/p5.Image.js | 7 ++++++- src/math/p5.Vector.js | 41 +++++++++++++++++++++---------------- src/math/patch-vector.js | 4 ++-- test/unit/math/p5.Vector.js | 41 +++++++++++++++++++------------------ 5 files changed, 67 insertions(+), 50 deletions(-) diff --git a/src/core/main.js b/src/core/main.js index 969c3a2ba1..a379f61155 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -50,7 +50,7 @@ class p5 { constructor(sketch, node) { // Apply addon defined decorations if(p5.decorations.size > 0){ - decorateClass(p5, p5.decorations, 'p5'); + decorateClass(p5, p5.decorations, 'p5', p5); p5.decorations.clear(); } @@ -520,7 +520,7 @@ function createBindGlobal(instance) { } // Generic function to decorate classes -function decorateClass(Target, decorations, path){ +function decorateClass(Target, decorations, path, p5){ path ??= Target.name; // Static properties for(const key in Target){ @@ -532,7 +532,8 @@ function decorateClass(Target, decorations, path){ const result = decorator(Target[key], { kind: 'method', name: key, - static: true + static: true, + p5 }); if(result){ Object.defineProperty(Target, key, { @@ -545,7 +546,8 @@ function decorateClass(Target, decorations, path){ const result = decorator(undefined, { kind: 'field', name: key, - static: true + static: true, + p5 }); if(result && typeof result === 'function'){ Target[key] = result(Target[key]); @@ -555,7 +557,7 @@ function decorateClass(Target, decorations, path){ } if(typeof Target[key] === 'function' && Target[key].prototype){ - decorateClass(Target[key], decorations, `${path}.${key}`); + decorateClass(Target[key], decorations, `${path}.${key}`, p5); } } } @@ -570,7 +572,8 @@ function decorateClass(Target, decorations, path){ const result = decorator(Target.prototype[member], { kind: 'method', name: member, - static: false + static: false, + p5 }); if(result) { Object.defineProperty(Target.prototype, member, { @@ -588,7 +591,8 @@ function decorateClass(Target, decorations, path){ const result = decorator(undefined, { kind: 'field', name: member, - static: false + static: false, + p5 }); Object.defineProperty(Target.prototype, member, { enumerable: true, @@ -602,12 +606,14 @@ function decorateClass(Target, decorations, path){ const getterResult = decorator(get, { kind: 'getter', name: member, - static: false + static: false, + p5 }); const setterResult = decorator(set, { kind: 'setter', name: member, - static: false + static: false, + p5 }); Object.defineProperty(Target.prototype, member, { enumerable: true, diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js index 80e5781524..92a67b77ff 100644 --- a/src/image/p5.Image.js +++ b/src/image/p5.Image.js @@ -31,6 +31,9 @@ class Image { this.pixels = []; } + // This will get overwritten when exported as part of p5. + _friendlyError(_e) {} + /** * Gets or sets the pixel density for high pixel density displays. * @@ -1605,7 +1608,7 @@ class Image { props.displayIndex = index; this.drawingContext.putImageData(props.frames[index].image, 0, 0); } else { - p5._friendlyError( + this._friendlyError( 'Cannot set GIF to a frame number that is higher than total number of frames or below zero.', 'setFrame' ); @@ -2105,6 +2108,8 @@ function image(p5, fn){ */ p5.Image = Image; + Image.prototype._friendlyError = p5._friendlyError; + /** * The image's width in pixels. * diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index e3fa481032..7126c2b5ff 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -5,7 +5,7 @@ import * as constants from '../core/constants'; -/** +/** * This function is used by binary vector operations to prioritize shorter vectors, * and to emit a warning when lengths do not match. */ @@ -14,7 +14,7 @@ const prioritizeSmallerDimension = function(currentVectorDimension, args) { //if (args.length !== currentVectorDimension && args.length !== 1) { // TODO how to suppress for valid solo arguments? - // p5._friendlyError( + // this._friendlyError( // `Operating on two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as ${minDimension}D vectors, and any additional values of the longer vector will be ignored.`, 'p5.Vector' //); //} @@ -55,7 +55,7 @@ class Vector { constructor(...args) { if (args.length === 0) { - p5._friendlyError( + this._friendlyError( 'Requires valid arguments.', 'p5.Vector' ); } @@ -69,7 +69,7 @@ class Vector { this.values = []; if(Array.isArray(args) && !args.every(v => typeof v === 'number' && Number.isFinite(v))){ - p5._friendlyError( + this._friendlyError( 'Arguments contain non-finite numbers', target.name ); @@ -85,6 +85,9 @@ class Vector { this.isVector = true; } + // This will get overwritten when exported as part of p5. + _friendlyError(_e) {} + get dimensions(){ return this.values.length; } @@ -123,7 +126,7 @@ class Vector { if (index < this.values.length) { return this.values[index]; } else { - p5._friendlyError( + this._friendlyError( 'The index parameter is trying to set a value outside the bounds of the vector', 'p5.Vector.setValue' ); @@ -148,7 +151,7 @@ class Vector { if (index < this.values.length) { this.values[index] = value; } else { - p5._friendlyError( + this._friendlyError( 'The index parameter is trying to set a value outside the bounds of the vector', 'p5.Vector.setValue' ); @@ -3051,7 +3054,7 @@ class Vector { if (!target) { target = v1.copy(); if (arguments.length === 3) { - p5._friendlyError( + this._friendlyError( 'The target parameter is undefined, it should be of type p5.Vector', 'p5.Vector.add' ); @@ -3098,7 +3101,7 @@ class Vector { if (!target) { target = v1.copy(); if (arguments.length === 3) { - p5._friendlyError( + this._friendlyError( 'The target parameter is undefined, it should be of type p5.Vector', 'p5.Vector.sub' ); @@ -3142,7 +3145,7 @@ class Vector { if (!target) { target = v.copy(); if (arguments.length === 3) { - p5._friendlyError( + this._friendlyError( 'The target parameter is undefined, it should be of type p5.Vector', 'p5.Vector.mult' ); @@ -3168,7 +3171,7 @@ class Vector { target = v.copy(); } else { if (!(target instanceof Vector)) { - p5._friendlyError( + this._friendlyError( 'The target parameter should be of type p5.Vector', 'p5.Vector.rotate' ); @@ -3212,7 +3215,7 @@ class Vector { target = v.copy(); if (arguments.length === 3) { - p5._friendlyError( + this._friendlyError( 'The target parameter is undefined, it should be of type p5.Vector', 'p5.Vector.div' ); @@ -3280,7 +3283,7 @@ class Vector { if (!target) { target = v1.copy(); if (arguments.length === 4) { - p5._friendlyError( + this._friendlyError( 'The target parameter is undefined, it should be of type p5.Vector', 'p5.Vector.lerp' ); @@ -3310,7 +3313,7 @@ class Vector { if (!target) { target = v1.copy(); if (arguments.length === 4) { - p5._friendlyError( + this._friendlyError( 'The target parameter is undefined, it should be of type p5.Vector', 'p5.Vector.slerp' ); @@ -3364,7 +3367,7 @@ class Vector { target = v.copy(); } else { if (!(target instanceof Vector)) { - p5._friendlyError( + this._friendlyError( 'The target parameter should be of type p5.Vector', 'p5.Vector.normalize' ); @@ -3390,7 +3393,7 @@ class Vector { target = v.copy(); } else { if (!(target instanceof Vector)) { - p5._friendlyError( + this._friendlyError( 'The target parameter should be of type p5.Vector', 'p5.Vector.limit' ); @@ -3416,7 +3419,7 @@ class Vector { target = v.copy(); } else { if (!(target instanceof Vector)) { - p5._friendlyError( + this._friendlyError( 'The target parameter should be of type p5.Vector', 'p5.Vector.setMag' ); @@ -3472,7 +3475,7 @@ class Vector { target = incidentVector.copy(); } else { if (!(target instanceof Vector)) { - p5._friendlyError( + this._friendlyError( 'The target parameter should be of type p5.Vector', 'p5.Vector.reflect' ); @@ -3513,7 +3516,7 @@ class Vector { } else if (v1 instanceof Array) { v = new Vector().set(v1); } else { - p5._friendlyError( + this._friendlyError( 'The v1 parameter should be of type Array or p5.Vector', 'p5.Vector.equals' ); @@ -3601,6 +3604,8 @@ function vector(p5, fn) { */ p5.Vector = Vector; + Vector.prototype._friendlyError = p5._friendlyError; + /** * The x component of the vector * @type {Number} diff --git a/src/math/patch-vector.js b/src/math/patch-vector.js index 8216c822e0..70398803f5 100644 --- a/src/math/patch-vector.js +++ b/src/math/patch-vector.js @@ -4,7 +4,7 @@ import { Vector } from './p5.Vector.js'; * @private * @internal */ -export function _defaultEmptyVector(target){ +export function _defaultEmptyVector(target, { p5 }){ return function(...args){ if(args.length === 0){ p5._friendlyError( @@ -27,7 +27,7 @@ export function _defaultEmptyVector(target){ * @internal */ export function _validatedVectorOperation(expectsSoloNumberArgument){ - return function(target){ + return function(target, { p5 }){ return function(...args){ if (args.length === 0) { // No arguments? No action diff --git a/test/unit/math/p5.Vector.js b/test/unit/math/p5.Vector.js index d0fe08bce5..df694d71a6 100644 --- a/test/unit/math/p5.Vector.js +++ b/test/unit/math/p5.Vector.js @@ -1,4 +1,4 @@ -import { Vector } from '../../../src/math/p5.Vector.js'; +import { default as vector, Vector } from '../../../src/math/p5.Vector.js'; import { default as math } from '../../../src/math/math.js'; import { _defaultEmptyVector, _validatedVectorOperation } from '../../../src/math/patch-vector.js'; import { vi } from 'vitest'; @@ -7,33 +7,34 @@ import { vi } from 'vitest'; suite('p5.Vector', function () { var v; - const mockP5 = {}; + let FESCalled = false; + const mockP5 = { + _friendlyError: function(msg, func) { + FESCalled = true; + console.warn(msg); + } + }; + const options = { p5: mockP5 }; const mockP5Prototype = {}; beforeAll(async function () { // Makes createVector available mockP5.Vector = Vector; math(mockP5, mockP5Prototype); + vector(mockP5, mockP5Prototype); // Ensures all decorators are used by unit tests mockP5Prototype.createVector = _defaultEmptyVector( - mockP5Prototype.createVector + mockP5Prototype.createVector, + options ); // The following mocks simulate the validation decorator - Vector.prototype.add = _validatedVectorOperation(false)(Vector.prototype.add); - Vector.prototype.sub = _validatedVectorOperation(false)(Vector.prototype.sub); - Vector.prototype.mult = _validatedVectorOperation(true)(Vector.prototype.mult); - Vector.prototype.rem = _validatedVectorOperation(true)(Vector.prototype.rem); - Vector.prototype.div = _validatedVectorOperation(true)(Vector.prototype.div); - - globalThis.FESCalled = false; - globalThis.p5 = { - _friendlyError: function(msg, func) { - globalThis.FESCalled = true; - console.warn(msg); - } - }; + Vector.prototype.add = _validatedVectorOperation(false)(Vector.prototype.add, options); + Vector.prototype.sub = _validatedVectorOperation(false)(Vector.prototype.sub, options); + Vector.prototype.mult = _validatedVectorOperation(true)(Vector.prototype.mult, options); + Vector.prototype.rem = _validatedVectorOperation(true)(Vector.prototype.rem, options); + Vector.prototype.div = _validatedVectorOperation(true)(Vector.prototype.div, options); }); afterEach(function () {}); @@ -2091,9 +2092,9 @@ suite('p5.Vector', function () { test('should throw friendly error if attempting to get element outside length', function () { let vect = new Vector(1, 2, 3, 4); - globalThis.FESCalled = false; + FESCalled = false; assert.equal(vect.getValue(5), undefined); - assert.equal(globalThis.FESCalled, true); + assert.equal(FESCalled, true); } ); }); @@ -2111,9 +2112,9 @@ suite('p5.Vector', function () { test('should throw friendly error if attempting to set element outside lenght', function () { let vect = new Vector(1, 2, 3, 4); - globalThis.FESCalled = false; + FESCalled = false; vect.setValue(100, 7); - assert.equal(globalThis.FESCalled, true); + assert.equal(FESCalled, true); } ); }); From 2ea8587e6da16e20b793cafbcf64661cf57b9c2d Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 10 Apr 2026 19:06:33 -0400 Subject: [PATCH 153/250] Fix reference to --- src/math/p5.Vector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 7126c2b5ff..c46c9dee64 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -71,7 +71,7 @@ class Vector { if(Array.isArray(args) && !args.every(v => typeof v === 'number' && Number.isFinite(v))){ this._friendlyError( 'Arguments contain non-finite numbers', - target.name + 'p5.Vector' ); } else { this.values = args; From 15e1c1e8025490b2bc7b3b2c8d1b1a02c7ac31bc Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 11 Apr 2026 10:13:30 -0400 Subject: [PATCH 154/250] Don't pass a p5 ref to decorators --- src/core/main.js | 24 +++++++++--------------- src/math/patch-vector.js | 8 ++++---- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/core/main.js b/src/core/main.js index a379f61155..969c3a2ba1 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -50,7 +50,7 @@ class p5 { constructor(sketch, node) { // Apply addon defined decorations if(p5.decorations.size > 0){ - decorateClass(p5, p5.decorations, 'p5', p5); + decorateClass(p5, p5.decorations, 'p5'); p5.decorations.clear(); } @@ -520,7 +520,7 @@ function createBindGlobal(instance) { } // Generic function to decorate classes -function decorateClass(Target, decorations, path, p5){ +function decorateClass(Target, decorations, path){ path ??= Target.name; // Static properties for(const key in Target){ @@ -532,8 +532,7 @@ function decorateClass(Target, decorations, path, p5){ const result = decorator(Target[key], { kind: 'method', name: key, - static: true, - p5 + static: true }); if(result){ Object.defineProperty(Target, key, { @@ -546,8 +545,7 @@ function decorateClass(Target, decorations, path, p5){ const result = decorator(undefined, { kind: 'field', name: key, - static: true, - p5 + static: true }); if(result && typeof result === 'function'){ Target[key] = result(Target[key]); @@ -557,7 +555,7 @@ function decorateClass(Target, decorations, path, p5){ } if(typeof Target[key] === 'function' && Target[key].prototype){ - decorateClass(Target[key], decorations, `${path}.${key}`, p5); + decorateClass(Target[key], decorations, `${path}.${key}`); } } } @@ -572,8 +570,7 @@ function decorateClass(Target, decorations, path, p5){ const result = decorator(Target.prototype[member], { kind: 'method', name: member, - static: false, - p5 + static: false }); if(result) { Object.defineProperty(Target.prototype, member, { @@ -591,8 +588,7 @@ function decorateClass(Target, decorations, path, p5){ const result = decorator(undefined, { kind: 'field', name: member, - static: false, - p5 + static: false }); Object.defineProperty(Target.prototype, member, { enumerable: true, @@ -606,14 +602,12 @@ function decorateClass(Target, decorations, path, p5){ const getterResult = decorator(get, { kind: 'getter', name: member, - static: false, - p5 + static: false }); const setterResult = decorator(set, { kind: 'setter', name: member, - static: false, - p5 + static: false }); Object.defineProperty(Target.prototype, member, { enumerable: true, diff --git a/src/math/patch-vector.js b/src/math/patch-vector.js index 70398803f5..d615027d07 100644 --- a/src/math/patch-vector.js +++ b/src/math/patch-vector.js @@ -4,10 +4,10 @@ import { Vector } from './p5.Vector.js'; * @private * @internal */ -export function _defaultEmptyVector(target, { p5 }){ +export function _defaultEmptyVector(target){ return function(...args){ if(args.length === 0){ - p5._friendlyError( + this._friendlyError( 'In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector.', 'p5.createVector' ); @@ -27,7 +27,7 @@ export function _defaultEmptyVector(target, { p5 }){ * @internal */ export function _validatedVectorOperation(expectsSoloNumberArgument){ - return function(target, { p5 }){ + return function(target){ return function(...args){ if (args.length === 0) { // No arguments? No action @@ -44,7 +44,7 @@ export function _validatedVectorOperation(expectsSoloNumberArgument){ } if(Array.isArray(args) && !args.every(v => typeof v === 'number' && Number.isFinite(v))){ - p5._friendlyError( + this._friendlyError( 'Arguments contain non-finite numbers', target.name ); From 0082cabd722c288b6cbd43aa46c8ce909913477a Mon Sep 17 00:00:00 2001 From: Kathrina-dev Date: Sun, 12 Apr 2026 20:56:04 +0000 Subject: [PATCH 155/250] added properties for all hooks --- src/strands/p5.strands.js | 69 +++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 4a86bfa55c..dd636a0ca3 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -215,7 +215,14 @@ if (typeof p5 !== "undefined") { /* ------------------------------------------------------------- */ /** * @typedef {Object} WorldInputsHook -*/ + * @property {any} position + * @property {any} normal + * @property {any} texCoord + * @property {any} color + * @property {function(): undefined} begin + * @property {function(): undefined} end + */ + /** * @property {WorldInputsHook} worldInputs * @beta @@ -263,7 +270,19 @@ if (typeof p5 !== "undefined") { /** * @typedef {Object} CombineColorsHook -*/ + * @property {any} baseColor + * @property {any} diffuse + * @property {any} ambientColor + * @property {any} ambient + * @property {any} specularColor + * @property {any} specular + * @property {any} emissive + * @property {any} opacity + * @property {function(): undefined} begin + * @property {function(): undefined} end + * @property {function(color: any): void} set + */ + /** * @property {CombineColorsHook} combineColors * @beta @@ -599,7 +618,23 @@ if (typeof p5 !== "undefined") { /** * @typedef {Object} PixelInputsHook -*/ + * @property {any} normal + * @property {any} texCoord + * @property {any} ambientLight + * @property {any} ambientMaterial + * @property {any} specularMaterial + * @property {any} emissiveMaterial + * @property {any} color + * @property {any} shininess + * @property {any} metalness + * @property {any} tangent + * @property {any} center + * @property {any} position + * @property {any} strokeWeight + * @property {function(): undefined} begin + * @property {function(): undefined} end + */ + /** * @property {PixelInputsHook} pixelInputs * @beta @@ -689,7 +724,15 @@ if (typeof p5 !== "undefined") { */ /** - * @property finalColor + * @typedef {Object} FinalColorHook + * @property {any} color + * @property {function(): undefined} begin + * @property {function(): undefined} end + * @property {function(color: any): void} set + */ + +/** + * @property {FinalColorHook} finalColor * @beta * @description * A shader hook block that modifies the final color of each pixel after all lighting is applied. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to adjust the color before it appears on the screen. Modifications happen between the `.begin()` and `.end()` methods of the hook. @@ -828,7 +871,14 @@ if (typeof p5 !== "undefined") { /** * @typedef {Object} ObjectInputsHook -*/ + * @property {any} position + * @property {any} normal + * @property {any} texCoord + * @property {any} color + * @property {function(): undefined} begin + * @property {function(): undefined} end + */ + /** * @property {ObjectInputsHook} objectInputs * @beta @@ -873,7 +923,14 @@ if (typeof p5 !== "undefined") { /** * @typedef {Object} CameraInputsHook -*/ + * @property {any} position + * @property {any} normal + * @property {any} texCoord + * @property {any} color + * @property {function(): undefined} begin + * @property {function(): undefined} end + */ + /** * @property {CameraInputsHook} cameraInputs * @beta From 5555f13a08e5fae27ee1f5b8b84cda29f73966e6 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 14 Apr 2026 21:06:36 -0400 Subject: [PATCH 156/250] Make sure swizzle assignment of storage properties works --- src/strands/strands_transpiler.js | 69 +++++++++++++++ test/unit/visual/cases/webgpu.js | 82 ++++++++++++++++++ .../000.png | Bin 0 -> 396 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 390 bytes .../metadata.json | 3 + 6 files changed, 157 insertions(+) create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader assigns to a swizzle of a struct vector field inside an if statement/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader assigns to a swizzle of a struct vector field inside an if statement/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader assigns to a swizzle of a struct vector field/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader assigns to a swizzle of a struct vector field/metadata.json diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index b30c3380e2..205b2350cd 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -1247,6 +1247,75 @@ const ASTCallbacks = { delete node.update; }, + // Swizzle assignments on storage fields like data[i].velocity.y *= -1 don't work + // as plain assignments because data.get(i).velocity returns a StrandsNode without + // an onRebind callback, so setting .y on it has no effect on the buffer. + // + // We detect this case here (ancestor walk is post-order, so by the time this runs + // data[i] has already been converted to data.get(i) by the MemberExpression visitor). + // buildPropertyPath returns null when it hits a non-Identifier object (like a .get() + // call), which distinguishes this from struct-field swizzles like inputs.position.x + // where buildPropertyPath returns a non-null path and the phi-node mechanism handles it. + // + // We rewrite fieldExpr.swizzle = rhs into a read-modify-write: + // let __tmp = fieldExpr + // __tmp.swizzle = rhs swizzleTrap.set mutates __tmp.id + // fieldExpr = __tmp proxy setter writes back to the buffer + ExpressionStatement(node, state, ancestors) { + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { + return; + } + const assign = node.expression; + if (assign?.type !== 'AssignmentExpression') return; + const left = assign.left; + if (left.type !== 'MemberExpression' || left.computed) return; + const propName = left.property.name; + if (!propName || !isSwizzle(propName)) return; + const fieldExpr = left.object; + // A plain identifier (e.g. myVec.y = 5) is handled directly by swizzleTrap.set. + if (fieldExpr.type === 'Identifier') return; + // A simple dotted path (e.g. inputs.position.x) is handled by the phi-node mechanism. + if (buildPropertyPath(fieldExpr) !== null) return; + + const tmpName = `__swizzle_tmp_${blockVarCounter++}`; + node.type = 'BlockStatement'; + node.body = [ + { + type: 'VariableDeclaration', + declarations: [{ + type: 'VariableDeclarator', + id: { type: 'Identifier', name: tmpName }, + init: JSON.parse(JSON.stringify(fieldExpr)), + }], + kind: 'let', + }, + { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + object: { type: 'Identifier', name: tmpName }, + property: { type: 'Identifier', name: propName }, + computed: false, + }, + right: assign.right, + }, + }, + { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: JSON.parse(JSON.stringify(fieldExpr)), + right: { type: 'Identifier', name: tmpName }, + }, + }, + ]; + delete node.expression; + }, + // Helper method to replace identifier references in AST nodes replaceIdentifierReferences(node, oldName, newName) { if (!node || typeof node !== 'object') return node; diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 93858f03c3..d13475b2c5 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -1422,6 +1422,88 @@ visualSuite("WebGPU", function () { await screenshot(); } ); + + visualTest( + 'Compute shader assigns to a swizzle of a struct vector field', + async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + const particles = p5.createStorage([ + { position: [15, 10] }, + ]); + + // Negate position.y via swizzle assignment + const computeShader = p5.buildComputeShader(() => { + const buf = p5.uniformStorage('buf', particles); + const idx = p5.index.x; + buf[idx].position.y *= -1; + }, { p5, particles }); + p5.compute(computeShader, 1); + + const sphereShader = p5.baseMaterialShader().modify(() => { + const buf = p5.uniformStorage('buf', particles); + p5.getWorldInputs((inputs) => { + const pos = buf[0].position; + inputs.position.x += pos.x; + inputs.position.y += pos.y; + return inputs; + }); + }, { p5, particles }); + + const geo = p5.buildGeometry(() => p5.sphere(5)); + p5.background(200); + p5.noStroke(); + p5.fill(255, 0, 0); + p5.shader(sphereShader); + p5.model(geo, 1); + + await screenshot(); + } + ); + + visualTest( + 'Compute shader assigns to a swizzle of a struct vector field inside an if statement', + async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + const particles = p5.createStorage([ + { position: [0, 0], velocity: [5, 5] }, + ]); + + // Move by velocity, then negate velocity.y if position.y > 0. + // After 1st run: position=[5,5], velocity=[5,-5]. + // After 2nd run: position=[10,0], velocity=[5,-5]. + const computeShader = p5.buildComputeShader(() => { + const buf = p5.uniformStorage('buf', particles); + const idx = p5.index.x; + buf[idx].position += buf[idx].velocity; + if (buf[idx].position.y > 0) { + buf[idx].velocity.y *= -1; + } + }, { p5, particles }); + p5.compute(computeShader, 1); + p5.compute(computeShader, 1); + + const sphereShader = p5.baseMaterialShader().modify(() => { + const buf = p5.uniformStorage('buf', particles); + p5.getWorldInputs((inputs) => { + const pos = buf[0].position; + inputs.position.x += pos.x; + inputs.position.y += pos.y; + return inputs; + }); + }, { p5, particles }); + + const geo = p5.buildGeometry(() => p5.sphere(5)); + p5.background(200); + p5.noStroke(); + p5.fill(255, 0, 0); + p5.shader(sphereShader); + p5.model(geo, 1); + + await screenshot(); + } + ); }); visualSuite('Feedback', function() { diff --git a/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader assigns to a swizzle of a struct vector field inside an if statement/000.png b/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader assigns to a swizzle of a struct vector field inside an if statement/000.png new file mode 100644 index 0000000000000000000000000000000000000000..2bf313d4bf2143e8e74af28305908a634c5768b0 GIT binary patch literal 396 zcmV;70dxL|P)oOf=BAJX$zLtq3TbJn| z6Zv3l($a1i+_7!lsjlA*<3r#7e3xk=5Dejf5#<=~M-bFxP1Bo!Kqv&yi9`)hbM@)E z8OV10rKV!0bHGg3=H_M~8n-oj1k4OX1pKQv1G!gKltHscE-?eSHH~PP?H>wQIcx?3 zx4*%lc2&TdgFpa;LNK5(N5lBibq+PmwaqLM5eUH$e)is>VGJ0lYkPALtq0xY9cY+L7gChCB_R(#5jQ9c_tEAV!Xgai~|UsUriz} q00030|4$afxc~qF21!IgR09CFOXFkf;P0UT0000uyrwF()#`dB8i+PWV$9)`i4+tyv``a4jc`aZeSTp|z*;eeTx zqdtxxSettDwY2QlYq54VYNp6o}+@C|vr+6i6sIw>Aax ztg1!*(l@3+9!;|d@)BT8fxzt)4A$-ftSJ!00ih5KD9kZX-@49W4fELeCrIr4RXqrX z@T>O@12te$hnWOP36|CfA~~bgXChk8OZPGn$r-Ia6VY;Bx|fMa&S>?Sh?euxy-Y-M zMyt<6w49gjWg?O@T79dDWcxvoY|9F7U?PPL&OV-rWLs8v0~0B1aQ5+=N#p|n0RR8! kTf}<+000I_L_t&o06M7SV_$dgz5oCK07*qoM6N<$g1J7VPXGV_ literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader assigns to a swizzle of a struct vector field/metadata.json b/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader assigns to a swizzle of a struct vector field/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Compute shaders/Compute shader assigns to a swizzle of a struct vector field/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file From 77bd69dff93c590bbd90089c7a19aba654ff9bb8 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 15 Apr 2026 20:01:03 -0400 Subject: [PATCH 157/250] Use an onRebind callback instead of transpiler tricks --- src/strands/ir_builders.js | 23 ++++++++++- src/strands/strands_transpiler.js | 69 ------------------------------- 2 files changed, 22 insertions(+), 70 deletions(-) diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index 48898d50c1..465572bebb 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -689,7 +689,28 @@ export function createStructArrayElementProxy(strandsContext, bufferNode, indexN }); const id = DAG.getOrCreateNode(dag, nodeData); CFG.recordInBasicBlock(cfg, cfg.currentBlock, id); - return createStrandsNode(id, field.dim, strandsContext); + // When a swizzle assignment fires (e.g. buf[i].vel.y *= -1), onRebind + // receives the new vector ID and writes it back to the buffer field, + // equivalent to buf[i].vel = newVec. + const onRebind = (newFieldID) => { + const accessData = DAG.createNodeData({ + nodeType: NodeType.OPERATION, + opCode: OpCode.Binary.ARRAY_ACCESS, + dependsOn: [bufferNode.id, index.id], + dimension: field.dim, + baseType: BaseType.FLOAT, + identifier: field.name, + }); + const accessID = DAG.getOrCreateNode(dag, accessData); + const assignData = DAG.createNodeData({ + nodeType: NodeType.ASSIGNMENT, + dependsOn: [accessID, newFieldID], + phiBlocks: [], + }); + const assignID = DAG.getOrCreateNode(dag, assignData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, assignID); + }; + return createStrandsNode(id, field.dim, strandsContext, onRebind); }, set(val) { // Create access node as assignment target (field name in identifier) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index 205b2350cd..b30c3380e2 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -1247,75 +1247,6 @@ const ASTCallbacks = { delete node.update; }, - // Swizzle assignments on storage fields like data[i].velocity.y *= -1 don't work - // as plain assignments because data.get(i).velocity returns a StrandsNode without - // an onRebind callback, so setting .y on it has no effect on the buffer. - // - // We detect this case here (ancestor walk is post-order, so by the time this runs - // data[i] has already been converted to data.get(i) by the MemberExpression visitor). - // buildPropertyPath returns null when it hits a non-Identifier object (like a .get() - // call), which distinguishes this from struct-field swizzles like inputs.position.x - // where buildPropertyPath returns a non-null path and the phi-node mechanism handles it. - // - // We rewrite fieldExpr.swizzle = rhs into a read-modify-write: - // let __tmp = fieldExpr - // __tmp.swizzle = rhs swizzleTrap.set mutates __tmp.id - // fieldExpr = __tmp proxy setter writes back to the buffer - ExpressionStatement(node, state, ancestors) { - if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { - return; - } - const assign = node.expression; - if (assign?.type !== 'AssignmentExpression') return; - const left = assign.left; - if (left.type !== 'MemberExpression' || left.computed) return; - const propName = left.property.name; - if (!propName || !isSwizzle(propName)) return; - const fieldExpr = left.object; - // A plain identifier (e.g. myVec.y = 5) is handled directly by swizzleTrap.set. - if (fieldExpr.type === 'Identifier') return; - // A simple dotted path (e.g. inputs.position.x) is handled by the phi-node mechanism. - if (buildPropertyPath(fieldExpr) !== null) return; - - const tmpName = `__swizzle_tmp_${blockVarCounter++}`; - node.type = 'BlockStatement'; - node.body = [ - { - type: 'VariableDeclaration', - declarations: [{ - type: 'VariableDeclarator', - id: { type: 'Identifier', name: tmpName }, - init: JSON.parse(JSON.stringify(fieldExpr)), - }], - kind: 'let', - }, - { - type: 'ExpressionStatement', - expression: { - type: 'AssignmentExpression', - operator: '=', - left: { - type: 'MemberExpression', - object: { type: 'Identifier', name: tmpName }, - property: { type: 'Identifier', name: propName }, - computed: false, - }, - right: assign.right, - }, - }, - { - type: 'ExpressionStatement', - expression: { - type: 'AssignmentExpression', - operator: '=', - left: JSON.parse(JSON.stringify(fieldExpr)), - right: { type: 'Identifier', name: tmpName }, - }, - }, - ]; - delete node.expression; - }, - // Helper method to replace identifier references in AST nodes replaceIdentifierReferences(node, oldName, newName) { if (!node || typeof node !== 'object') return node; From d9898f49cf328bd60d869cb53a508c82adf9078e Mon Sep 17 00:00:00 2001 From: kushal Date: Thu, 16 Apr 2026 17:13:22 +0530 Subject: [PATCH 158/250] added loop.protect error message --- src/strands/strands_transpiler.js | 60 +++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index b30c3380e2..78250268e5 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -2,6 +2,7 @@ import { parse } from 'acorn'; import { ancestor, recursive } from 'acorn-walk'; import escodegen from 'escodegen'; import { UnarySymbolToName } from './ir_types'; +import * as FES from './strands_FES'; let blockVarCounter = 0; let loopVarCounter = 0; function replaceBinaryOperator(codeSource) { @@ -98,7 +99,64 @@ function nodeIsVarying(node) { ) ); } +// Convert static member expressions into dotted paths such as +// `loopProtect.protect` so loop-protection calls can be matched reliably. +function getMemberExpressionPath(node) { + if (node?.type === 'Identifier') return node.name; + // Computed properties like `obj[prop]` are not safe to match as fixed paths. + if (node?.type !== 'MemberExpression' || node.computed) return null; + + const objectPath = getMemberExpressionPath(node.object); + const propertyName = node.property?.name; + + return objectPath && propertyName + ? `${objectPath}.${propertyName}` + : null; +} + +// Detect calls added by loop protection before Strands tries to transpile them. +function isLoopProtectionCall(node) { + if (node?.type !== 'CallExpression') return false; + + const path = getMemberExpressionPath(node.callee); + + if (!path) return false; + + return ( + path === 'loopProtect.protect' || + path.endsWith('.loopProtect') || + path.endsWith('.loopProtect.protect') + ); +} + +// Scan AST for loop-protection injection and throw with `// noprotect` hint. +function throwIfLoopProtectionInserted(ast) { + let found = false; + + ancestor(ast, { + CallExpression(node) { + if (isLoopProtectionCall(node)) { + found = true; + } + }, + LogicalExpression(node) { + // Loop protection may appear as the right side of a short-circuit check. + if ( + node.right?.type === 'CallExpression' && + isLoopProtectionCall(node.right) + ) { + found = true; + } + } + }); + + if (found) { + FES.internalError( + 'loop protection error Loop protection code detected. Add `// noprotect` at the top of your sketch and run again.' + ); + } +} // Helper function to check if a statement is a variable declaration with strands control flow init function statementContainsStrandsControlFlow(stmt) { // Check for variable declarations with strands control flow init @@ -1697,6 +1755,8 @@ export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { locations: srcLocations }); + throwIfLoopProtectionInserted(ast); + // Pre-pass: collect names of functions passed by reference as uniform callbacks const uniformCallbackNames = collectUniformCallbackNames(ast); From 8678c507d47353fa4cf036d95831ec8d0535a7d8 Mon Sep 17 00:00:00 2001 From: Kathrina-dev Date: Thu, 16 Apr 2026 15:23:34 +0000 Subject: [PATCH 159/250] added texCoord property in finalColorHook --- src/strands/p5.strands.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 0bbff2f850..f72aedc5a8 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -725,6 +725,7 @@ if (typeof p5 !== "undefined") { /** * @typedef {Object} FinalColorHook * @property {any} color + * @property {any} texCoord * @property {function(): undefined} begin * @property {function(): undefined} end * @property {function(color: any): void} set @@ -738,6 +739,7 @@ if (typeof p5 !== "undefined") { * * `finalColor` has the following properties: * - `color`: a four-component vector representing the pixel color (red, green, blue, alpha). + * - `texCoord`: a two-component vector representing the texture coordinates (u, v) * * Call `.set()` on the hook with a vector with four components (red, green, blue, alpha) to update the final color. * From d650e1c5ccebd5952d494e5cef287f01d3d69759 Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Fri, 17 Apr 2026 00:38:44 +0530 Subject: [PATCH 160/250] feat(webgpu): add read() to p5.StorageBuffer with tests --- src/webgpu/p5.RendererWebGPU.js | 118 ++++++++++++++++++++++++++ test/unit/webgpu/p5.RendererWebGPU.js | 85 +++++++++++++++++++ 2 files changed, 203 insertions(+) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 59723c5023..41381ba3fb 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -173,6 +173,124 @@ function rendererWebGPU(p5, fn) { device.queue.writeBuffer(this.buffer, 0, floatData); } } + + /** + * Reads data from a storage buffer back into JavaScript. + * + * Copies data from the GPU to the CPU using a temporary buffer, + * so it must be awaited. Returns a `Float32Array` for number + * buffers, or an array of plain objects for struct buffers. + * + * Note: This is a GPU -> CPU read, so calling it often (like every frame) + * can be slow. + * + * ```js example + * let data; + * let computeShader; + * + * async function setup() { + * await createCanvas(100, 100, WEBGPU); + * + * data = createStorage(new Float32Array([1, 2, 3, 4])); + * computeShader = buildComputeShader(doubleValues); + * compute(computeShader, 4); + * + * let result = await data.read(); + * // result is Float32Array [2, 4, 6, 8] + * for (let i = 0; i < result.length; i++) { + * print(result[i]); + * } + * describe('Prints the values 2, 4, 6, 8 to the console.'); + * } + * + * function doubleValues() { + * let d = uniformStorage(data); + * let idx = index.x; + * d[idx] = d[idx] * 2; + * } + * ``` + * + * @method read + * @for p5.StorageBuffer + * @beta + * @webgpu + * @webgpuOnly + * @returns {Promise} + */ + async read() { + const device = this._renderer.device; + this._renderer.flushDraw(); + + const stagingBuffer = device.createBuffer({ + size: this.size, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + + const commandEncoder = device.createCommandEncoder(); + commandEncoder.copyBufferToBuffer(this.buffer, 0, stagingBuffer, 0, this.size); + device.queue.submit([commandEncoder.finish()]); + + await stagingBuffer.mapAsync(GPUMapMode.READ, 0, this.size); + const mappedRange = stagingBuffer.getMappedRange(0, this.size); + + // Copy before unmapping because mapped memory becomes invalid after unmap + const rawCopy = new Float32Array(mappedRange.byteLength / 4); + rawCopy.set(new Float32Array(mappedRange)); + + stagingBuffer.unmap(); + stagingBuffer.destroy(); + + if (this._schema !== null) { + return this._unpackStructArray(rawCopy, this._schema); + } + return rawCopy; + } + + // Inverse of _packStructArray reads packed buffer back into plain JS objects + // using the same schema layout - fields, stride and offsets + _unpackStructArray(floatView, schema) { + const { fields, stride } = schema; + const dataView = new DataView(floatView.buffer); + const count = Math.floor(floatView.byteLength / stride); + const result = []; + + for (let i = 0; i < count; i++) { + const item = {}; + const baseOffset = i * stride; + for (const field of fields) { + const byteOffset = baseOffset + field.offset; + const n = field.size / 4; + + if (field.baseType === 'u32') { + if (n === 1) { + item[field.name] = dataView.getUint32(byteOffset, true); + } else { + item[field.name] = Array.from({ length: n }, (_, j) => + dataView.getUint32(byteOffset + j * 4, true) + ); + } + } else if (field.baseType === 'i32') { + if (n === 1) { + item[field.name] = dataView.getInt32(byteOffset, true); + } else { + item[field.name] = Array.from({ length: n }, (_, j) => + dataView.getInt32(byteOffset + j * 4, true) + ); + } + } else { + const idx = byteOffset / 4; + if (n === 1) { + item[field.name] = floatView[idx]; + } else { + item[field.name] = Array.from(floatView.slice(idx, idx + n)); + } + } + } + result.push(item); + } + + return result; + } } /** diff --git a/test/unit/webgpu/p5.RendererWebGPU.js b/test/unit/webgpu/p5.RendererWebGPU.js index 1ed62563c5..0612d22e12 100644 --- a/test/unit/webgpu/p5.RendererWebGPU.js +++ b/test/unit/webgpu/p5.RendererWebGPU.js @@ -160,4 +160,89 @@ suite('WebGPU p5.RendererWebGPU', function() { expect(myp5._renderer).to.exist; }); }); + + suite('StorageBuffer.read()', function() { + test('reads back float array data', async function() { + const input = new Float32Array([1, 2, 3, 4]); + const buf = myp5.createStorage(input); + + const result = await buf.read(); + + expect(result).to.be.instanceOf(Float32Array); + expect(result.length).to.equal(input.length); + for (let i = 0; i < input.length; i++) { + expect(result[i]).to.be.closeTo(input[i], 0.001); + } + }); + + test('reads back struct array data', async function() { + const input = [ + { x: 1.0, y: 2.0 }, + { x: 3.0, y: 4.0 }, + ]; + const buf = myp5.createStorage(input); + + const result = await buf.read(); + + expect(result).to.be.an('array'); + expect(result.length).to.equal(input.length); + for (let i = 0; i < input.length; i++) { + expect(result[i].x).to.be.closeTo(input[i].x, 0.001); + expect(result[i].y).to.be.closeTo(input[i].y, 0.001); + } + }); + + test('read after update returns new data', async function() { + const buf = myp5.createStorage(new Float32Array([10, 20, 30])); + const updated = new Float32Array([100, 200, 300]); + buf.update(updated); + + const result = await buf.read(); + + for (let i = 0; i < updated.length; i++) { + expect(result[i]).to.be.closeTo(updated[i], 0.001); + } + }); + + test('reads back struct with vector fields', async function() { + const input = [ + { position: myp5.createVector(1, 2), speed: 5.0 }, + { position: myp5.createVector(3, 4), speed: 10.0 }, + ]; + const buf = myp5.createStorage(input); + + const result = await buf.read(); + + expect(result).to.be.an('array'); + expect(result.length).to.equal(2); + // Vector fields come back as plain arrays + expect(result[0].position[0]).to.be.closeTo(1, 0.001); + expect(result[0].position[1]).to.be.closeTo(2, 0.001); + expect(result[0].speed).to.be.closeTo(5.0, 0.001); + expect(result[1].position[0]).to.be.closeTo(3, 0.001); + expect(result[1].position[1]).to.be.closeTo(4, 0.001); + expect(result[1].speed).to.be.closeTo(10.0, 0.001); + }); + + test('reads back data modified by a compute shader', async function() { + const input = new Float32Array([1, 2, 3, 4]); + const buf = myp5.createStorage(input); + + const computeShader = myp5.buildComputeShader(() => { + const d = myp5.uniformStorage(); + const idx = myp5.index.x; + d[idx] = d[idx] * 2; + }, { myp5 }); + + computeShader.setUniform('d', buf); + myp5.compute(computeShader, 4); + + const result = await buf.read(); + + expect(result).to.be.instanceOf(Float32Array); + for (let i = 0; i < input.length; i++) { + expect(result[i]).to.be.closeTo(input[i] * 2, 0.001); + } + }); + }); }); From 1aeeaefae68314fbe2b81b853828578f24a0c92d Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Fri, 17 Apr 2026 03:28:30 +0530 Subject: [PATCH 161/250] refactor(webgpu): move unpackStructArray to renderer --- src/webgpu/p5.RendererWebGPU.js | 94 ++++++++++++++++----------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 41381ba3fb..dc8da80468 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -241,56 +241,10 @@ function rendererWebGPU(p5, fn) { stagingBuffer.destroy(); if (this._schema !== null) { - return this._unpackStructArray(rawCopy, this._schema); + return this._renderer._unpackStructArray(rawCopy, this._schema); } return rawCopy; } - - // Inverse of _packStructArray reads packed buffer back into plain JS objects - // using the same schema layout - fields, stride and offsets - _unpackStructArray(floatView, schema) { - const { fields, stride } = schema; - const dataView = new DataView(floatView.buffer); - const count = Math.floor(floatView.byteLength / stride); - const result = []; - - for (let i = 0; i < count; i++) { - const item = {}; - const baseOffset = i * stride; - for (const field of fields) { - const byteOffset = baseOffset + field.offset; - const n = field.size / 4; - - if (field.baseType === 'u32') { - if (n === 1) { - item[field.name] = dataView.getUint32(byteOffset, true); - } else { - item[field.name] = Array.from({ length: n }, (_, j) => - dataView.getUint32(byteOffset + j * 4, true) - ); - } - } else if (field.baseType === 'i32') { - if (n === 1) { - item[field.name] = dataView.getInt32(byteOffset, true); - } else { - item[field.name] = Array.from({ length: n }, (_, j) => - dataView.getInt32(byteOffset + j * 4, true) - ); - } - } else { - const idx = byteOffset / 4; - if (n === 1) { - item[field.name] = floatView[idx]; - } else { - item[field.name] = Array.from(floatView.slice(idx, idx + n)); - } - } - } - result.push(item); - } - - return result; - } } /** @@ -3398,6 +3352,52 @@ ${hookUniformFields}} return floatView; } + // Inverse of _packStructArray reads packed buffer back into plain JS objects + // using the same schema layout - fields, stride and offsets + _unpackStructArray(floatView, schema) { + const { fields, stride } = schema; + const dataView = new DataView(floatView.buffer); + const count = Math.floor(floatView.byteLength / stride); + const result = []; + + for (let i = 0; i < count; i++) { + const item = {}; + const baseOffset = i * stride; + for (const field of fields) { + const byteOffset = baseOffset + field.offset; + const n = field.size / 4; + + if (field.baseType === 'u32') { + if (n === 1) { + item[field.name] = dataView.getUint32(byteOffset, true); + } else { + item[field.name] = Array.from({ length: n }, (_, j) => + dataView.getUint32(byteOffset + j * 4, true) + ); + } + } else if (field.baseType === 'i32') { + if (n === 1) { + item[field.name] = dataView.getInt32(byteOffset, true); + } else { + item[field.name] = Array.from({ length: n }, (_, j) => + dataView.getInt32(byteOffset + j * 4, true) + ); + } + } else { + const idx = byteOffset / 4; + if (n === 1) { + item[field.name] = floatView[idx]; + } else { + item[field.name] = Array.from(floatView.slice(idx, idx + n)); + } + } + } + result.push(item); + } + + return result; + } + createStorage(dataOrCount) { const device = this.device; From 5996f678085e9a155ef7e62a93d839e09a7a655c Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Fri, 17 Apr 2026 12:06:50 +0530 Subject: [PATCH 162/250] Refactor: extract replaceIdentifierReferences and remove this binding from ASTCallbacks --- src/strands/strands_transpiler.js | 63 +++++++++++++++---------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index b30c3380e2..09a59f4c20 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -235,6 +235,30 @@ function replaceReferences(node, tempVarMap) { internalReplaceReferences(node); } +function replaceIdentifierReferences(node, oldName, newName) { + if (!node || typeof node !== 'object') return node; + + const replaceInNode = (n) => { + if (!n || typeof n !== 'object') return n; + if (n.type === 'Identifier' && n.name === oldName) { + return { ...n, name: newName }; + } + const newNode = { ...n }; + for (const key in n) { + if (n.hasOwnProperty(key) && key !== 'parent') { + if (Array.isArray(n[key])) { + newNode[key] = n[key].map(replaceInNode); + } else if (typeof n[key] === 'object') { + newNode[key] = replaceInNode(n[key]); + } + } + } + return newNode; + }; + + return replaceInNode(node); +} + const ASTCallbacks = { UnaryExpression(node, state, ancestors) { if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { @@ -896,8 +920,8 @@ const ASTCallbacks = { // Replace the update expression with the assignment expression Object.assign(node, assignmentExpr); delete node.prefix; - this.BinaryExpression(node.right, state, [...ancestors, node]); - this.AssignmentExpression(node, state, ancestors); + ASTCallbacks.BinaryExpression(node.right, state, [...ancestors, node]); + ASTCallbacks.AssignmentExpression(node, state, ancestors); }, ForStatement(node, state, ancestors) { if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { @@ -954,7 +978,7 @@ const ASTCallbacks = { // Replace loop variable references with the parameter if (node.init?.type === 'VariableDeclaration') { const loopVarName = node.init.declarations[0].id.name; - conditionBody = this.replaceIdentifierReferences(conditionBody, loopVarName, uniqueLoopVar); + conditionBody = replaceIdentifierReferences(conditionBody, loopVarName, uniqueLoopVar); } const conditionAst = { type: 'Program', body: [{ type: 'ExpressionStatement', expression: conditionBody }] }; conditionBody = conditionAst.body[0].expression; @@ -972,7 +996,7 @@ const ASTCallbacks = { // Replace loop variable references with the parameter if (node.init?.type === 'VariableDeclaration') { const loopVarName = node.init.declarations[0].id.name; - updateExpr = this.replaceIdentifierReferences(updateExpr, loopVarName, uniqueLoopVar); + updateExpr = replaceIdentifierReferences(updateExpr, loopVarName, uniqueLoopVar); } const updateAst = { type: 'Program', body: [{ type: 'ExpressionStatement', expression: updateExpr }] }; updateExpr = updateAst.body[0].expression; @@ -1011,7 +1035,7 @@ const ASTCallbacks = { // Replace loop variable references in the body if (node.init?.type === 'VariableDeclaration') { const loopVarName = node.init.declarations[0].id.name; - bodyBlock = this.replaceIdentifierReferences(bodyBlock, loopVarName, uniqueLoopVar); + bodyBlock = replaceIdentifierReferences(bodyBlock, loopVarName, uniqueLoopVar); } const bodyFunction = { @@ -1247,33 +1271,8 @@ const ASTCallbacks = { delete node.update; }, - // Helper method to replace identifier references in AST nodes - replaceIdentifierReferences(node, oldName, newName) { - if (!node || typeof node !== 'object') return node; - - const replaceInNode = (n) => { - if (!n || typeof n !== 'object') return n; - - if (n.type === 'Identifier' && n.name === oldName) { - return { ...n, name: newName }; - } - - // Create a copy and recursively process properties - const newNode = { ...n }; - for (const key in n) { - if (n.hasOwnProperty(key) && key !== 'parent') { - if (Array.isArray(n[key])) { - newNode[key] = n[key].map(replaceInNode); - } else if (typeof n[key] === 'object') { - newNode[key] = replaceInNode(n[key]); - } - } - } - return newNode; - }; - - return replaceInNode(node); - } + + } // Helper function to check if a function body contains return statements in control flow From fea06a0970eb787401e01cfc77ab4568ffc0e735 Mon Sep 17 00:00:00 2001 From: nityam Date: Fri, 17 Apr 2026 13:18:29 +0530 Subject: [PATCH 163/250] support mixed material coloring using sentinel for uncolored vertices --- src/webgl/loading.js | 14 ++------------ test/unit/io/loadModel.js | 23 ++++++++++++++++------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/webgl/loading.js b/src/webgl/loading.js index cb0d04878f..69f4df032f 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -631,6 +631,7 @@ function loading(p5, fn){ model.vertexColors.push(1); } else { hasColorlessVertices = true; + model.vertexColors.push(-1, -1, -1, -1); } } else { face.push(usedVerts[vertString][currentMaterial]); @@ -652,18 +653,7 @@ function loading(p5, fn){ if (model.vertexNormals.length === 0) { model.computeNormals(); } - if (hasColoredVertices && hasColorlessVertices) { - // Mixed model: some faces have a material diffuse color assigned, others do not. - // This is common in real-world OBJ exports (e.g. Blender, Sketchfab) where only - // some mesh groups have an explicit MTL material. Rather than crashing the sketch, - // we degrade gracefully: strip the partial vertex colors so the model renders - // with the default fill color instead of corrupted per-vertex coloring. - console.warn( - 'p5.js: This OBJ model has mixed material coloring — some faces have a ' + - 'material diffuse color and some do not. Vertex colors will not be applied. ' + - 'Consider assigning a material to every face group in your 3D software, ' + - 'or use a model where all faces share the same material.' - ); + if (!hasColoredVertices) { model.vertexColors = []; } diff --git a/test/unit/io/loadModel.js b/test/unit/io/loadModel.js index e010390586..590a12bb1e 100644 --- a/test/unit/io/loadModel.js +++ b/test/unit/io/loadModel.js @@ -79,18 +79,27 @@ suite('loadModel', function() { assert.deepEqual(model.vertexColors, expectedColors); }); - test('mixed material coloring loads model with empty vertexColors instead of crashing', async function() { - // eg1.obj has some faces without a material and some with one. - // Real-world exports from Blender/Sketchfab frequently produce this structure. - // The loader should degrade gracefully rather than throwing, so beginners' - // sketches don't crash when loading common 3D assets. + test('mixed material coloring loads model with sentinel colors for uncolored vertices', async function() { const model = await mockP5Prototype.loadModel(inconsistentColorObjFile); assert.instanceOf(model, Geometry); assert.equal( model.vertexColors.length, - 0, - 'Mixed-material model should have no vertex colors (graceful degradation)' + model.vertices.length * 4, + 'vertexColors should have four entries per vertex' ); + const hasSentinel = model.vertexColors.some( + (_, i) => + i % 4 === 0 && + model.vertexColors[i] === -1 && + model.vertexColors[i + 1] === -1 && + model.vertexColors[i + 2] === -1 && + model.vertexColors[i + 3] === -1 + ); + const hasRealColor = model.vertexColors.some( + (_, i) => i % 4 === 0 && model.vertexColors[i] !== -1 + ); + assert.isTrue(hasSentinel, 'Uncolored vertices should have sentinel color'); + assert.isTrue(hasRealColor, 'Colored vertices should retain their color'); }); test('missing MTL file shows OBJ model without vertexColors', async function() { From 91904917f4992269299cbed592fa2fed6a7f3e94 Mon Sep 17 00:00:00 2001 From: nityam Date: Fri, 17 Apr 2026 15:26:59 +0530 Subject: [PATCH 164/250] prompt before tessellating very large shapes --- src/core/p5.Renderer3D.js | 2 + src/webgl/ShapeBuilder.js | 23 ++++++++ test/unit/webgl/p5.RendererGL.js | 92 ++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 1fde0670bc..21d246eca0 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -221,6 +221,8 @@ export class Renderer3D extends Renderer { // Used by beginShape/endShape functions to construct a p5.Geometry this.shapeBuilder = new ShapeBuilder(this); + this._largeTessellationAcknowledged = false; + this.geometryBufferCache = new GeometryBufferCache(this); this.curStrokeCap = constants.ROUND; diff --git a/src/webgl/ShapeBuilder.js b/src/webgl/ShapeBuilder.js index 124fa62bfa..6a2c723f78 100644 --- a/src/webgl/ShapeBuilder.js +++ b/src/webgl/ShapeBuilder.js @@ -148,6 +148,29 @@ export class ShapeBuilder { } if (this.shapeMode === constants.PATH) { + const vertexCount = this.geometry.vertices.length; + const MAX_SAFE_TESSELLATION_VERTICES = 50000; + + if (vertexCount > MAX_SAFE_TESSELLATION_VERTICES) { + const p5Class = this.renderer._pInst.constructor; + if ( + !p5Class.disableFriendlyErrors && + !this.renderer._largeTessellationAcknowledged + ) { + const proceed = window.confirm( + '🌸 p5.js says:\n\n' + + `This shape has ${vertexCount} vertices. Tessellating shapes with this ` + + 'many vertices can be very slow and may cause your browser to become ' + + 'unresponsive.\n\n' + + 'Do you want to continue tessellating this shape?' + ); + if (!proceed) { + return; + } + this.renderer._largeTessellationAcknowledged = true; + } + } + this.isProcessingVertices = true; this._tesselateShape(); this.isProcessingVertices = false; diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 4d3eaaf7aa..22ede1f326 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -2032,6 +2032,98 @@ suite('p5.RendererGL', function() { [-10, 0, 10] ); }); + + suite('large tessellation guard', function() { + test('prompts user before tessellating >50k vertices', function() { + const renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false); + const tessSpy = vi.spyOn( + renderer.shapeBuilder, + '_tesselateShape' + ).mockImplementation(() => {}); + + myp5.beginShape(myp5.TESS); + for (let i = 0; i < 60000; i++) { + myp5.vertex(i % 100, Math.floor(i / 100), 0); + } + myp5.endShape(); + + expect(confirmSpy).toHaveBeenCalled(); + expect(confirmSpy.mock.calls[0][0]).toContain('60000'); + expect(tessSpy).not.toHaveBeenCalled(); + + confirmSpy.mockRestore(); + tessSpy.mockRestore(); + }); + + test('only prompts once when user approves large tessellation', function() { + const renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true); + const tessSpy = vi.spyOn( + renderer.shapeBuilder, + '_tesselateShape' + ).mockImplementation(() => {}); + + myp5.beginShape(myp5.TESS); + for (let i = 0; i < 60000; i++) { + myp5.vertex(i % 100, Math.floor(i / 100), 0); + } + myp5.endShape(); + + expect(confirmSpy).toHaveBeenCalledTimes(1); + expect(renderer._largeTessellationAcknowledged).toBe(true); + + myp5.beginShape(myp5.TESS); + for (let i = 0; i < 60000; i++) { + myp5.vertex(i % 100, Math.floor(i / 100), 0); + } + myp5.endShape(); + + expect(confirmSpy).toHaveBeenCalledTimes(1); + + confirmSpy.mockRestore(); + tessSpy.mockRestore(); + }); + + test('skips prompt when p5.disableFriendlyErrors is true', function() { + const renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false); + const tessSpy = vi.spyOn( + renderer.shapeBuilder, + '_tesselateShape' + ).mockImplementation(() => {}); + p5.disableFriendlyErrors = true; + + myp5.beginShape(myp5.TESS); + for (let i = 0; i < 60000; i++) { + myp5.vertex(i % 100, Math.floor(i / 100), 0); + } + myp5.endShape(); + + expect(confirmSpy).not.toHaveBeenCalled(); + expect(tessSpy).toHaveBeenCalled(); + + p5.disableFriendlyErrors = false; + confirmSpy.mockRestore(); + tessSpy.mockRestore(); + }); + + test('works normally for <50k vertices', function() { + const renderer = myp5.createCanvas(10, 10, myp5.WEBGL); + const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false); + + myp5.beginShape(myp5.TESS); + myp5.vertex(-10, -10, 0); + myp5.vertex(10, -10, 0); + myp5.vertex(10, 10, 0); + myp5.vertex(-10, 10, 0); + myp5.endShape(myp5.CLOSE); + + expect(confirmSpy).not.toHaveBeenCalled(); + + confirmSpy.mockRestore(); + }); + }); }); suite('color interpolation', function() { From 746e46c2e004ee1c306a404a9ce220760f9287bd Mon Sep 17 00:00:00 2001 From: nityam Date: Fri, 17 Apr 2026 15:47:54 +0530 Subject: [PATCH 165/250] use beginShape without TESS arg for dev-2.0 path mode --- test/unit/webgl/p5.RendererGL.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 22ede1f326..c0e2e94a49 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -2042,7 +2042,7 @@ suite('p5.RendererGL', function() { '_tesselateShape' ).mockImplementation(() => {}); - myp5.beginShape(myp5.TESS); + myp5.beginShape(); for (let i = 0; i < 60000; i++) { myp5.vertex(i % 100, Math.floor(i / 100), 0); } @@ -2064,7 +2064,7 @@ suite('p5.RendererGL', function() { '_tesselateShape' ).mockImplementation(() => {}); - myp5.beginShape(myp5.TESS); + myp5.beginShape(); for (let i = 0; i < 60000; i++) { myp5.vertex(i % 100, Math.floor(i / 100), 0); } @@ -2073,7 +2073,7 @@ suite('p5.RendererGL', function() { expect(confirmSpy).toHaveBeenCalledTimes(1); expect(renderer._largeTessellationAcknowledged).toBe(true); - myp5.beginShape(myp5.TESS); + myp5.beginShape(); for (let i = 0; i < 60000; i++) { myp5.vertex(i % 100, Math.floor(i / 100), 0); } @@ -2094,7 +2094,7 @@ suite('p5.RendererGL', function() { ).mockImplementation(() => {}); p5.disableFriendlyErrors = true; - myp5.beginShape(myp5.TESS); + myp5.beginShape(); for (let i = 0; i < 60000; i++) { myp5.vertex(i % 100, Math.floor(i / 100), 0); } @@ -2112,7 +2112,7 @@ suite('p5.RendererGL', function() { const renderer = myp5.createCanvas(10, 10, myp5.WEBGL); const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false); - myp5.beginShape(myp5.TESS); + myp5.beginShape(); myp5.vertex(-10, -10, 0); myp5.vertex(10, -10, 0); myp5.vertex(10, 10, 0); From 8ac5502538a58704eceff1a4534227b199fd20a4 Mon Sep 17 00:00:00 2001 From: Perminder Singh Date: Sat, 18 Apr 2026 21:12:18 +0530 Subject: [PATCH 166/250] implementing random function for strands --- src/image/filterRenderer2D.js | 10 ++ src/strands/p5.strands.js | 4 + src/strands/strands_api.js | 85 +++++++++ src/webgl/p5.RendererGL.js | 10 ++ src/webgl/shaders/functions/randomGLSL.glsl | 23 +++ .../shaders/functions/randomVertGLSL.glsl | 21 +++ src/webgpu/p5.RendererWebGPU.js | 166 ++++++++++++++---- .../shaders/functions/randomComputeWGSL.js | 25 +++ .../shaders/functions/randomVertWGSL.js | 24 +++ src/webgpu/shaders/functions/randomWGSL.js | 24 +++ src/webgpu/strands_wgslBackend.js | 12 ++ 11 files changed, 367 insertions(+), 37 deletions(-) create mode 100644 src/webgl/shaders/functions/randomGLSL.glsl create mode 100644 src/webgl/shaders/functions/randomVertGLSL.glsl create mode 100644 src/webgpu/shaders/functions/randomComputeWGSL.js create mode 100644 src/webgpu/shaders/functions/randomVertWGSL.js create mode 100644 src/webgpu/shaders/functions/randomWGSL.js diff --git a/src/image/filterRenderer2D.js b/src/image/filterRenderer2D.js index d6bf72eed3..5f00cb919f 100644 --- a/src/image/filterRenderer2D.js +++ b/src/image/filterRenderer2D.js @@ -18,6 +18,8 @@ import webgl2CompatibilityShader from '../webgl/shaders/webgl2Compatibility.glsl import { glslBackend } from '../webgl/strands_glslBackend'; import { getShaderHookTypes } from '../webgl/shaderHookUtils'; import noiseGLSL from '../webgl/shaders/functions/noise3DGLSL.glsl'; +import randomGLSL from '../webgl/shaders/functions/randomGLSL.glsl'; +import randomVertGLSL from '../webgl/shaders/functions/randomVertGLSL.glsl'; import { makeFilterShader } from '../core/filterShaders'; class FilterRenderer2D { @@ -310,6 +312,14 @@ class FilterRenderer2D { return noiseGLSL; } + getRandomFragmentShaderSnippet() { + return randomGLSL; + } + + getRandomVertexShaderSnippet() { + return randomVertGLSL; + } + /** * Set the current filter operation and parameter. If a customShader is provided, * that overrides the operation-based shader. diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index d8c839847e..5b13893880 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -50,6 +50,8 @@ function strands(p5, fn) { ctx.windowOverrides = {}; ctx.fnOverrides = {}; ctx.graphicsOverrides = {}; + ctx._randomCallCount = 0; + ctx._randomSeed = null; if (active) { p5.disableFriendlyErrors = true; } @@ -65,6 +67,8 @@ function strands(p5, fn) { ctx.computeDeclarations = new Set(); ctx.hooks = []; ctx.active = false; + ctx._randomCallCount = 0; + ctx._randomSeed = null; p5.disableFriendlyErrors = ctx.previousFES; for (const key in ctx.windowOverrides) { window[key] = ctx.windowOverrides[key]; diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index ab1453eb1c..2f36372d0a 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -318,6 +318,8 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { // Add noise function with backend-agnostic implementation const originalNoise = fn.noise; const originalNoiseDetail = fn.noiseDetail; + const originalRandom = fn.random; + const originalRandomSeed = fn.randomSeed; const originalMillis = fn.millis; strandsContext._noiseOctaves = null; @@ -382,6 +384,89 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return createStrandsNode(id, dimension, strandsContext); }); + strandsContext._randomSeed = null; + strandsContext._randomCallCount = 0; + + augmentFn(fn, p5, 'randomSeed', function (seed) { + if (!strandsContext.active) { + return originalRandomSeed.apply(this, arguments); + } + strandsContext._randomSeed = seed; + }); + + augmentFn(fn, p5, 'random', function (...args) { + if (!strandsContext.active) { + return originalRandom.apply(this, args); + } + + const randomVertSnippet = this._renderer.getRandomVertexShaderSnippet(); + const randomFragSnippet = this._renderer.getRandomFragmentShaderSnippet(); + + strandsContext.vertexDeclarations.add(randomVertSnippet); + strandsContext.fragmentDeclarations.add(randomFragSnippet); + + if (this._renderer.getRandomComputeShaderSnippet) { + const randomComputeSnippet = this._renderer.getRandomComputeShaderSnippet(); + strandsContext.computeDeclarations.add(randomComputeSnippet); + } + + let seedNode; + if (strandsContext._randomSeed !== null && strandsContext._randomSeed.isStrandsNode) { + seedNode = strandsContext._randomSeed; + } else { + const userSeed = strandsContext._randomSeed; + seedNode = getOrCreateUniformNode( + strandsContext, + '_p5_randomSeed', + DataType.float1, + userSeed !== null + ? () => userSeed + : () => performance.now(), + ); + } + + const callIndex = strandsContext._randomCallCount++; + + const nodeArgs = [seedNode, callIndex]; + + if (args.length === 0) { + const { id, dimension } = build.functionCallNode(strandsContext, 'random', nodeArgs, { + overloads: [{ + params: [DataType.float1, DataType.float1], + returnType: DataType.float1, + }] + }); + return createStrandsNode(id, dimension, strandsContext); + } else if (args.length === 1) { + // random(max) → [0, max) + const rawNode = build.functionCallNode(strandsContext, 'random', nodeArgs, { + overloads: [{ + params: [DataType.float1, DataType.float1], + returnType: DataType.float1, + }] + }); + const rawStrandsNode = createStrandsNode(rawNode.id, rawNode.dimension, strandsContext); + return rawStrandsNode.mult(p5.strandsNode(args[0])); + } else if (args.length === 2) { + // random(min, max) → [min, max) + const rawNode = build.functionCallNode(strandsContext, 'random', nodeArgs, { + overloads: [{ + params: [DataType.float1, DataType.float1], + returnType: DataType.float1, + }] + }); + const rawStrandsNode = createStrandsNode(rawNode.id, rawNode.dimension, strandsContext); + const minNode = p5.strandsNode(args[0]); + const maxNode = p5.strandsNode(args[1]); + // min + raw * (max - min) + return rawStrandsNode.mult(maxNode.sub(minNode)).add(minNode); + } else { + p5._friendlyError( + `It looks like you've called random() with ${args.length} arguments. In strands, random() supports 0, 1, or 2 numeric arguments.` + ); + } + }); + augmentFn(fn, p5, 'millis', function (...args) { if (!strandsContext.active) { return originalMillis.apply(this, args); diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index b744baff19..af85208e80 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -20,6 +20,8 @@ import { glslBackend } from './strands_glslBackend'; import { TypeInfoFromGLSLName } from '../strands/ir_types.js'; import { getShaderHookTypes } from './shaderHookUtils'; import noiseGLSL from './shaders/functions/noise3DGLSL.glsl'; +import randomGLSL from './shaders/functions/randomGLSL.glsl'; +import randomVertGLSL from './shaders/functions/randomVertGLSL.glsl'; import filterBaseVert from "./shaders/filters/base.vert"; import lightingShader from "./shaders/lighting.glsl"; @@ -1909,6 +1911,14 @@ class RendererGL extends Renderer3D { return noiseGLSL; } + getRandomFragmentShaderSnippet() { + return randomGLSL; + } + + getRandomVertexShaderSnippet() { + return randomVertGLSL; + } + } function rendererGL(p5, fn) { diff --git a/src/webgl/shaders/functions/randomGLSL.glsl b/src/webgl/shaders/functions/randomGLSL.glsl new file mode 100644 index 0000000000..58bdc2fc96 --- /dev/null +++ b/src/webgl/shaders/functions/randomGLSL.glsl @@ -0,0 +1,23 @@ +// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW) +// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) +// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal) +// α₂ = 1/φ₂² = 0.5698402910 +// 1/φ = 0.6180339887 (golden ratio conjugate) + +float _p5_hash(vec3 p) { + p = fract(p * vec3(0.1031, 0.1030, 0.0973)); + p += dot(p, p.yxz + 33.33); + return fract((p.x + p.y) * p.z); +} + +float random(float seed, float callIndex) { + vec2 pixelCoord = gl_FragCoord.xy; + // fract(seed * α₁) normalizes large seeds (e.g. performance.now()) into [0,1) + // and spreads them optimally via the R₂ sequence's plastic constant + float s = fract(seed * 0.7548776662); + return _p5_hash(vec3( + pixelCoord.x + s, + pixelCoord.y + callIndex * 0.5698402910, + s + callIndex * 0.6180339887 + )); +} diff --git a/src/webgl/shaders/functions/randomVertGLSL.glsl b/src/webgl/shaders/functions/randomVertGLSL.glsl new file mode 100644 index 0000000000..2940be1b96 --- /dev/null +++ b/src/webgl/shaders/functions/randomVertGLSL.glsl @@ -0,0 +1,21 @@ +// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW) +// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) +// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal) +// α₂ = 1/φ₂² = 0.5698402910 +// 1/φ = 0.6180339887 (golden ratio conjugate) + +float _p5_hash(vec3 p) { + p = fract(p * vec3(0.1031, 0.1030, 0.0973)); + p += dot(p, p.yxz + 33.33); + return fract((p.x + p.y) * p.z); +} + +float random(float seed, float callIndex) { + float vid = float(gl_VertexID); + float s = fract(seed * 0.7548776662); + return _p5_hash(vec3( + vid + s, + vid * 0.5698402910 + callIndex * 0.6180339887, + s + callIndex * 0.7548776662 + )); +} diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 61f8580ac6..a562ee371a 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -15,6 +15,9 @@ import { fontVertexShader, fontFragmentShader } from './shaders/font'; import { blitVertexShader, blitFragmentShader } from './shaders/blit'; import { wgslBackend } from './strands_wgslBackend'; import noiseWGSL from './shaders/functions/noise3DWGSL'; +import randomWGSL from './shaders/functions/randomWGSL'; +import randomVertWGSL from './shaders/functions/randomVertWGSL'; +import randomComputeWGSL from './shaders/functions/randomComputeWGSL'; import { baseFilterVertexShader, baseFilterFragmentShader } from './shaders/filters/base'; import { imageLightVertexShader, imageLightDiffusedFragmentShader, imageLightSpecularFragmentShader } from './shaders/imageLight'; import { baseComputeShader } from './shaders/compute'; @@ -2533,10 +2536,86 @@ function rendererWebGPU(p5, fn) { ); let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment|compute)\s*(?:@workgroup_size\([^)]+\)\s*)?)?fn main[^{]+\{)/); + + const getBuiltinParamName = (mainSrc, builtinName) => { + const match = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`).exec(mainSrc); + return match ? match[1] : null; + }; + + const ensureBuiltinParam = (mainSrc, builtinName, fallbackName, typeName) => { + const existingName = getBuiltinParamName(mainSrc, builtinName); + if (existingName) { + return { mainSrc, argName: existingName }; + } + + const hasParams = /\(\s*\S/.test(mainSrc); + const injectedMain = mainSrc.replace( + /\)\s*(->|\{)/, + `${hasParams ? ', ' : ''}@builtin(${builtinName}) ${fallbackName}: ${typeName}) $1` + ); + + return { mainSrc: injectedMain, argName: fallbackName }; + }; + + const getMainStructParameter = (mainSrc) => { + const match = /fn main\s*\(\s*(\w+)\s*:\s*(\w+)/.exec(mainSrc); + if (!match) return null; + return { inputName: match[1], structName: match[2] }; + }; + + const getStructBuiltinFieldName = (structName, builtinName) => { + const structMatch = new RegExp(`struct\\s+${structName}\\s*\\{([^}]*)\\}`, 's').exec(preMain); + if (!structMatch) return null; + const fieldMatch = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`, 's').exec(structMatch[1]); + return fieldMatch ? fieldMatch[1] : null; + }; + + const appendHookParams = (params, additionalParams) => { + if (additionalParams.length === 0) return params; + const hasParams = !/^\(\s*\)$/.test(params); + return `${params.slice(0, -1)}${hasParams ? ', ' : ''}${additionalParams.join(', ')})`; + }; + + let hookExtraParams = []; + let hookExtraArgs = []; + if (shaderType === 'vertex') { - if (!main.match(/\@builtin\s*\(\s*instance_index\s*\)/)) { - main = main.replace(/\)\s*(->|\{)/, ', @builtin(instance_index) instanceID: u32) $1'); + const ensuredInstance = ensureBuiltinParam(main, 'instance_index', 'instanceID', 'u32'); + main = ensuredInstance.mainSrc; + + const ensuredVertex = ensureBuiltinParam(main, 'vertex_index', '_p5VertexId', 'u32'); + main = ensuredVertex.mainSrc; + + hookExtraParams = ['instanceID: u32', '_p5VertexId: u32']; + hookExtraArgs = [ensuredInstance.argName, ensuredVertex.argName]; + } else if (shaderType === 'fragment') { + const directPositionArg = getBuiltinParamName(main, 'position'); + let fragmentPositionArg = directPositionArg; + + if (!fragmentPositionArg) { + const mainStructParam = getMainStructParameter(main); + if (mainStructParam) { + const positionField = getStructBuiltinFieldName(mainStructParam.structName, 'position'); + if (positionField) { + fragmentPositionArg = `${mainStructParam.inputName}.${positionField}`; + } + } } + + if (!fragmentPositionArg) { + const ensuredPosition = ensureBuiltinParam(main, 'position', '_p5FragPos', 'vec4'); + main = ensuredPosition.mainSrc; + fragmentPositionArg = ensuredPosition.argName; + } + + hookExtraParams = ['_p5FragPos: vec4']; + hookExtraArgs = [fragmentPositionArg]; + } else if (shaderType === 'compute') { + const ensuredGlobalId = ensureBuiltinParam(main, 'global_invocation_id', '_p5GlobalId', 'vec3'); + main = ensuredGlobalId.mainSrc; + + hookExtraParams = ['_p5GlobalId: vec3']; + hookExtraArgs = [ensuredGlobalId.argName]; } // Inject hook uniforms as a separate struct at a new binding @@ -2724,11 +2803,7 @@ ${hookUniformFields}} let [_, params, body] = /^(\([^\)]*\))((?:.|\n)*)$/.exec(shader.hooks[shaderType][hookDef]); - if (shaderType === 'vertex') { - // Splice the instance ID in as a final parameter to every WGSL hook function - let hasParams = !!params.match(/^\(\s*\S+.*\)$/); - params = params.slice(0, -1) + (hasParams ? ', ' : '') + 'instanceID: u32)'; - } + params = appendHookParams(params, hookExtraParams); if (hookType === 'void') { hooks += `fn HOOK_${hookName}${params}${body}\n`; @@ -2737,40 +2812,45 @@ ${hookUniformFields}} } } - // Add the instance ID as a final parameter to each hook call - if (shaderType === 'vertex') { - const addInstanceIDParam = (src) => { - let result = src; - let idx = 0; - let match; - do { - match = /HOOK_\w+\(/.exec(result.slice(idx)); - if (match) { - idx += match.index + match[0].length - 1; - let nesting = 0; - let hasParams = false; - while (idx < result.length) { - if (result[idx] === '(') { - nesting++; - } else if (result[idx] === ')') { - nesting--; - } else if (result[idx].match(/\S/)) { - hasParams = true; - } - idx++; - if (nesting === 0) { - break; - } + // Pass stage-specific builtins from main to each hook call. + // Collect ALL HOOK_ calls (including nested ones) then insert + // extra args from right to left so position shifts don't + // invalidate earlier insertion points. + if (hookExtraArgs.length > 0) { + const addHookArgs = (src) => { + const insertions = []; + let searchIdx = 0; + let m; + while ((m = /HOOK_\w+\(/.exec(src.slice(searchIdx))) !== null) { + const openParen = searchIdx + m.index + m[0].length - 1; + let pos = openParen + 1; + let nesting = 1; + let hasParams = false; + while (pos < src.length && nesting > 0) { + if (src[pos] === '(') nesting++; + else if (src[pos] === ')') { + nesting--; + if (nesting === 0) break; + } else if (/\S/.test(src[pos])) { + hasParams = true; } - const insertion = (hasParams ? ', ' : '') + 'instanceID'; - result = result.slice(0, idx-1) + insertion + result.slice(idx-1); - idx += insertion.length; + pos++; } - } while (match); + insertions.push({ pos, hasParams }); + searchIdx = openParen + 1; + } + + insertions.sort((a, b) => b.pos - a.pos); + + let result = src; + for (const { pos, hasParams } of insertions) { + const insertion = (hasParams ? ', ' : '') + hookExtraArgs.join(', '); + result = result.slice(0, pos) + insertion + result.slice(pos); + } return result; }; - preMain = addInstanceIDParam(preMain); - postMain = addInstanceIDParam(postMain); + preMain = addHookArgs(preMain); + postMain = addHookArgs(postMain); } return preMain + '\n' + defines + hooks + main + postMain; @@ -3654,6 +3734,18 @@ ${hookUniformFields}} return noiseWGSL; } + getRandomFragmentShaderSnippet() { + return randomWGSL; + } + + getRandomVertexShaderSnippet() { + return randomVertWGSL; + } + + getRandomComputeShaderSnippet() { + return randomComputeWGSL; + } + baseFilterShader() { if (!this._baseFilterShader) { diff --git a/src/webgpu/shaders/functions/randomComputeWGSL.js b/src/webgpu/shaders/functions/randomComputeWGSL.js new file mode 100644 index 0000000000..9b9b17379c --- /dev/null +++ b/src/webgpu/shaders/functions/randomComputeWGSL.js @@ -0,0 +1,25 @@ +// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW) +// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) +// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal) +// α₂ = 1/φ₂² = 0.5698402910 +// 1/φ = 0.6180339887 (golden ratio conjugate) +// +// Compute shader version: invocationId is passed in from main via @builtin(global_invocation_id) + +export default ` +fn _p5_hash(p: vec3) -> f32 { + var p3 = fract(p * vec3(0.1031, 0.1030, 0.0973)); + p3 = p3 + dot(p3, p3.yxz + 33.33); + return fract((p3.x + p3.y) * p3.z); +} + +fn random(seed: f32, callIndex: f32, invocationId: vec3) -> f32 { + let id = vec3(invocationId); + let s = fract(seed * 0.7548776662); + return _p5_hash(vec3( + id.x + s, + id.y + callIndex * 0.5698402910, + id.z + s + callIndex * 0.6180339887 + )); +} +`; diff --git a/src/webgpu/shaders/functions/randomVertWGSL.js b/src/webgpu/shaders/functions/randomVertWGSL.js new file mode 100644 index 0000000000..008a5448cd --- /dev/null +++ b/src/webgpu/shaders/functions/randomVertWGSL.js @@ -0,0 +1,24 @@ +// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW) +// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) +// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal) +// α₂ = 1/φ₂² = 0.5698402910 +// 1/φ = 0.6180339887 (golden ratio conjugate) +// +// Vertex shader version: vertexId is passed in from main via @builtin(vertex_index) + +export default ` +fn _p5_hash(p: vec3) -> f32 { + var p3 = fract(p * vec3(0.1031, 0.1030, 0.0973)); + p3 = p3 + dot(p3, p3.yxz + 33.33); + return fract((p3.x + p3.y) * p3.z); +} + +fn random(seed: f32, callIndex: f32, vertexId: f32) -> f32 { + let s = fract(seed * 0.7548776662); + return _p5_hash(vec3( + vertexId + s, + vertexId * 0.5698402910 + callIndex * 0.6180339887, + s + callIndex * 0.7548776662 + )); +} +`; diff --git a/src/webgpu/shaders/functions/randomWGSL.js b/src/webgpu/shaders/functions/randomWGSL.js new file mode 100644 index 0000000000..3e0b69d289 --- /dev/null +++ b/src/webgpu/shaders/functions/randomWGSL.js @@ -0,0 +1,24 @@ +// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW) +// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) +// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal) +// α₂ = 1/φ₂² = 0.5698402910 +// 1/φ = 0.6180339887 (golden ratio conjugate) +// +// Fragment shader version: pixelCoord is passed in from main via @builtin(position) + +export default ` +fn _p5_hash(p: vec3) -> f32 { + var p3 = fract(p * vec3(0.1031, 0.1030, 0.0973)); + p3 = p3 + dot(p3, p3.yxz + 33.33); + return fract((p3.x + p3.y) * p3.z); +} + +fn random(seed: f32, callIndex: f32, pixelCoord: vec2) -> f32 { + let s = fract(seed * 0.7548776662); + return _p5_hash(vec3( + pixelCoord.x + s, + pixelCoord.y + callIndex * 0.5698402910, + s + callIndex * 0.6180339887 + )); +} +`; diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index 32226fa83d..63366d900a 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -482,6 +482,18 @@ export const wgslBackend = { } const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg)); + + if (node.identifier === 'random') { + const ctx = generationContext.shaderContext; + if (ctx === 'fragment') { + functionArgs.push('_p5FragPos.xy'); + } else if (ctx === 'vertex') { + functionArgs.push('f32(_p5VertexId)'); + } else if (ctx === 'compute') { + functionArgs.push('_p5GlobalId'); + } + } + return `${node.identifier}(${functionArgs.join(', ')})`; } if (node.opCode === OpCode.Binary.MEMBER_ACCESS) { From 56f11d3efa8690ef74081cf3bc2f3eea0c42b087 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 19 Apr 2026 12:47:15 -0400 Subject: [PATCH 167/250] Fix framebuffers with depth: false --- src/webgpu/p5.RendererWebGPU.js | 51 ++++++++---------- test/unit/visual/cases/webgpu.js | 22 ++++++++ .../Framebuffer with depth disabled/000.png | Bin 0 -> 564 bytes .../metadata.json | 3 ++ 4 files changed, 48 insertions(+), 28 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGPU/Framebuffers/Framebuffer with depth disabled/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Framebuffers/Framebuffer with depth disabled/metadata.json diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 59723c5023..6be44c21c2 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -682,9 +682,9 @@ function rendererWebGPU(p5, fn) { 1; // No MSAA needed when blitting already-antialiased textures to canvas const sampleCount = this._getValidSampleCount(requestedSampleCount); - const depthFormat = activeFramebuffer && activeFramebuffer.useDepth ? - this._getWebGPUDepthFormat(activeFramebuffer) : - this.depthFormat; + const depthFormat = activeFramebuffer + ? (activeFramebuffer.useDepth ? this._getWebGPUDepthFormat(activeFramebuffer) : undefined) + : this.depthFormat; const drawTarget = this.drawTarget(); const clipping = this._clipping; @@ -761,25 +761,27 @@ function rendererWebGPU(p5, fn) { }, primitive: { topology }, multisample: { count: sampleCount }, - depthStencil: { - format: depthFormat, - depthWriteEnabled: !clipping, - depthCompare: 'less-equal', - stencilFront: { - compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'), - failOp: 'keep', - depthFailOp: 'keep', - passOp: clipping ? 'replace' : 'keep', + ...(depthFormat ? { + depthStencil: { + format: depthFormat, + depthWriteEnabled: !clipping, + depthCompare: 'less-equal', + stencilFront: { + compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'), + failOp: 'keep', + depthFailOp: 'keep', + passOp: clipping ? 'replace' : 'keep', + }, + stencilBack: { + compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'), + failOp: 'keep', + depthFailOp: 'keep', + passOp: clipping ? 'replace' : 'keep', + }, + stencilReadMask: 0xFF, + stencilWriteMask: clipping ? 0xFF : 0x00, }, - stencilBack: { - compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'), - failOp: 'keep', - depthFailOp: 'keep', - passOp: clipping ? 'replace' : 'keep', - }, - stencilReadMask: 0xFF, - stencilWriteMask: clipping ? 0xFF : 0x00, - }, + } : {}), }); shader._pipelineCache.set(key, pipeline); } @@ -3852,13 +3854,6 @@ ${hookUniformFields}} px = Math.ceil(Math.sqrt(totalIterations)); py = Math.ceil(totalIterations / px); pz = 1; - - if (p5.debug || exceedsLimits) { - console.warn( - `p5.js: Compute dispatch (${x}, ${y}, ${z}) auto-spread to (${px}, ${py}, 1) ` + - `to ${exceedsLimits ? 'stay within GPU limits' : 'optimize performance'}.` - ); - } } shader.setUniform('uPhysicalCount', [px, py, pz]); diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index d13475b2c5..9c57b54708 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -523,6 +523,28 @@ visualSuite("WebGPU", function () { }, ); + visualTest( + "Framebuffer with depth disabled", + async function (p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + const fbo = p5.createFramebuffer({ width: 50, height: 50, depth: false }); + + fbo.draw(() => { + p5.background(0, 0, 200); + p5.fill(255, 200, 0); + p5.noStroke(); + p5.circle(0, 0, 30); + }); + + p5.background(50); + p5.texture(fbo); + p5.noStroke(); + p5.plane(50, 50); + + await screenshot(); + }, + ); + visualTest( "Fixed-size framebuffer after manual resize", async function (p5, screenshot) { diff --git a/test/unit/visual/screenshots/WebGPU/Framebuffers/Framebuffer with depth disabled/000.png b/test/unit/visual/screenshots/WebGPU/Framebuffers/Framebuffer with depth disabled/000.png new file mode 100644 index 0000000000000000000000000000000000000000..20fb23ed5a13882994aba978551e5b4fb2b8350e GIT binary patch literal 564 zcmV-40?Yl0P)R?hK%QbKYNOJFJ-vB zLX05gCH^|d*2dT;(kIfW1N~|>YHV1xPh_rjpSx@MJs+RqHtY*=I$R1t&fNLv`?QHr zpaD?mV_0$DBM3pBf!SW|vl@hgDU1SHD+O^T3#bajrl0~$VF5yG`d;}Q2EmvD6tksV zw9s72S=tMMV2JUOaAXfnv?FI_5R5R4n0d~i9l5G92u7ZQYH}hT5(v|%DbRbCi4fj= z@+6Q*r!a|x>g5J;bSP5q>>2!s*@Vf)WuZ!r;{b#9h&3)~Y?M6+-dPd5m~eI3s{ zRtfbgOCWTJScBvw{!dCE0H~efj2aOS8H9;MjJz;ttJ6SR2H_A!7_!a`G|`S|hCndF z5M$OEffkxktuP3N7*hc2XmC~eHbg6`!MQ3B1SsHbKNJ+kWjc4a5qeIG`w$U|Ixgf<1C9f95H_P6v@INIK9#k{X#N(Cjy0000 Date: Sun, 19 Apr 2026 12:58:32 -0400 Subject: [PATCH 168/250] Fix noSmooth() --- src/shape/attributes.js | 8 +++++--- src/webgpu/p5.RendererWebGPU.js | 4 ++-- test/unit/visual/cases/webgpu.js | 15 +++++++++++++++ .../000.png | Bin 0 -> 438 bytes .../metadata.json | 3 +++ test/unit/webgpu/p5.RendererWebGPU.js | 7 +++++++ 6 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGPU/Rendering attributes/noSmooth() does not crash and disables antialiasing/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Rendering attributes/noSmooth() does not crash and disables antialiasing/metadata.json diff --git a/src/shape/attributes.js b/src/shape/attributes.js index ab5ec21a01..41fe554849 100644 --- a/src/shape/attributes.js +++ b/src/shape/attributes.js @@ -107,8 +107,10 @@ function attributes(p5, fn){ * In WebGL mode, `noSmooth()` causes all shapes to be drawn with jagged * (aliased) edges. The functions don't affect images or fonts. * + * Note: In WebGPU mode, you must `await` this function. + * * @method noSmooth - * @chainable + * @return {void|Promise} * * @example * let heart; @@ -162,10 +164,10 @@ function attributes(p5, fn){ if ('imageSmoothingEnabled' in this.drawingContext) { this.drawingContext.imageSmoothingEnabled = false; } + return this; } else { - this.setAttributes('antialias', false); + return this.setAttributes('antialias', false); } - return this; }; /** diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 6be44c21c2..96bdcffc39 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -319,7 +319,7 @@ function rendererWebGPU(p5, fn) { } if (this._pInst._webgpuAttributes[key] !== value) { //changing value of previously altered attribute - this._webgpuAttributes[key] = value; + this._pInst._webgpuAttributes[key] = value; unchanged = false; } //setting all attributes with some change @@ -2988,7 +2988,7 @@ ${hookUniformFields}} } defaultFramebufferAntialias() { - return true; + return this._pInst._webgpuAttributes?.antialias !== false; } supportsFramebufferAntialias() { diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 9c57b54708..0d1e1e6249 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -586,6 +586,21 @@ visualSuite("WebGPU", function () { ); }); + visualSuite("Rendering attributes", function () { + visualTest( + "noSmooth() does not crash and disables antialiasing", + async function (p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + await p5.noSmooth(); + p5.background(0); + p5.fill(255); + p5.noStroke(); + p5.circle(0, 0, 30); + await screenshot(); + }, + ); + }); + visualSuite("Clipping", function () { visualTest( "Basic clipping with circles", diff --git a/test/unit/visual/screenshots/WebGPU/Rendering attributes/noSmooth() does not crash and disables antialiasing/000.png b/test/unit/visual/screenshots/WebGPU/Rendering attributes/noSmooth() does not crash and disables antialiasing/000.png new file mode 100644 index 0000000000000000000000000000000000000000..5c5063d11879168928f11b351a47eb8a77182fcc GIT binary patch literal 438 zcmV;n0ZIOeP)I12fX_IDRY?G$dP2FVaF;jEuUd8eGiAd1M0+7QTyb3LeNZy6*i)B`B<5e*rn6kO>nfkd2&!m3&l zNN1==(@TRWTXBOZO`lke8{~=M6wM8yxHY48Z)AqciiUe5fYNt*ts7+eWS1cL&08Ae zVsU>Y5b!(Xo}f|)nwA7o(OlwM1_8~f2qowmGKdWZx{$r|1jKrWKs*D@t_g!c%()sM zZ~(P*_dX0_oq-{7>tO-d*H8?B{3t_g5Fo_}nln@HIS8t%)SkKuU4o zcUbs)F6$a+Iduz&JVnBT4U%ADYKy&*R67%z?TsXunA&1*B-PG@W}g560RR8ep3md} g000I_L_t&o0L Date: Sun, 19 Apr 2026 13:06:29 -0400 Subject: [PATCH 169/250] Document createCanvas must be awaited in webgpu mode --- docs/parameterData.json | 500 ++++++++++++++++++++-------------------- src/core/rendering.js | 12 +- test/types/webgpu.ts | 9 + 3 files changed, 273 insertions(+), 248 deletions(-) create mode 100644 test/types/webgpu.ts diff --git a/docs/parameterData.json b/docs/parameterData.json index bfad638c18..0bdd3fa6da 100644 --- a/docs/parameterData.json +++ b/docs/parameterData.json @@ -1,4 +1,48 @@ { + "p5.Color": { + "toString": { + "overloads": [ + [ + "String?" + ] + ] + }, + "contrast": { + "overloads": [ + [ + "Color" + ] + ] + }, + "setRed": { + "overloads": [ + [ + "Number" + ] + ] + }, + "setGreen": { + "overloads": [ + [ + "Number" + ] + ] + }, + "setBlue": { + "overloads": [ + [ + "Number" + ] + ] + }, + "setAlpha": { + "overloads": [ + [ + "Number" + ] + ] + } + }, "p5.Image": { "pixelDensity": { "overloads": [ @@ -170,50 +214,6 @@ ] } }, - "p5.Color": { - "toString": { - "overloads": [ - [ - "String?" - ] - ] - }, - "contrast": { - "overloads": [ - [ - "Color" - ] - ] - }, - "setRed": { - "overloads": [ - [ - "Number" - ] - ] - }, - "setGreen": { - "overloads": [ - [ - "Number" - ] - ] - }, - "setBlue": { - "overloads": [ - [ - "Number" - ] - ] - }, - "setAlpha": { - "overloads": [ - [ - "Number" - ] - ] - } - }, "p5": { "remove": { "overloads": [ @@ -382,21 +382,6 @@ ] ] }, - "createCanvas": { - "overloads": [ - [ - "Number?", - "Number?", - "P2D|WEBGL|P2DHDR|WEBGPU?", - "HTMLCanvasElement?" - ], - [ - "Number?", - "Number?", - "HTMLCanvasElement?" - ] - ] - }, "textOutput": { "overloads": [ [ @@ -431,6 +416,27 @@ ] ] }, + "createCanvas": { + "overloads": [ + [ + "Number?", + "Number?", + "P2D|WEBGL|P2DHDR?", + "HTMLCanvasElement?" + ], + [ + "Number", + "Number", + "WEBGPU", + "HTMLCanvasElement?" + ], + [ + "Number?", + "Number?", + "HTMLCanvasElement?" + ] + ] + }, "loadShader": { "overloads": [ [ @@ -546,6 +552,11 @@ ] ] }, + "endClip": { + "overloads": [ + [] + ] + }, "int": { "overloads": [ [ @@ -556,11 +567,6 @@ ] ] }, - "endClip": { - "overloads": [ - [] - ] - }, "copy": { "overloads": [ [ @@ -696,18 +702,6 @@ ] ] }, - "nfc": { - "overloads": [ - [ - "Number|String", - "Integer|String?" - ], - [ - "Number[]", - "Integer|String?" - ] - ] - }, "applyMatrix": { "overloads": [ [ @@ -741,6 +735,18 @@ ] ] }, + "nfc": { + "overloads": [ + [ + "Number|String", + "Integer|String?" + ], + [ + "Number[]", + "Integer|String?" + ] + ] + }, "push": { "overloads": [ [] @@ -773,15 +779,6 @@ ] ] }, - "resizeCanvas": { - "overloads": [ - [ - "Number", - "Number", - "Boolean?" - ] - ] - }, "month": { "overloads": [ [] @@ -810,6 +807,15 @@ ] ] }, + "resizeCanvas": { + "overloads": [ + [ + "Number", + "Number", + "Boolean?" + ] + ] + }, "saveCanvas": { "overloads": [ [ @@ -849,11 +855,6 @@ ] ] }, - "noCanvas": { - "overloads": [ - [] - ] - }, "second": { "overloads": [ [] @@ -890,6 +891,11 @@ ] ] }, + "noCanvas": { + "overloads": [ + [] + ] + }, "createElement": { "overloads": [ [ @@ -927,20 +933,20 @@ ] ] }, - "byte": { + "buildGeometry": { "overloads": [ [ - "String|Boolean|Number" - ], - [ - "Array" + "Function" ] ] }, - "buildGeometry": { + "byte": { "overloads": [ [ - "Function" + "String|Boolean|Number" + ], + [ + "Array" ] ] }, @@ -999,11 +1005,6 @@ ] ] }, - "smooth": { - "overloads": [ - [] - ] - }, "nfp": { "overloads": [ [ @@ -1018,6 +1019,11 @@ ] ] }, + "smooth": { + "overloads": [ + [] + ] + }, "clearStorage": { "overloads": [ [] @@ -1086,6 +1092,15 @@ ] ] }, + "lerp": { + "overloads": [ + [ + "Number", + "Number", + "Number" + ] + ] + }, "createGraphics": { "overloads": [ [ @@ -1101,15 +1116,6 @@ ] ] }, - "lerp": { - "overloads": [ - [ - "Number", - "Number", - "Number" - ] - ] - }, "frameRate": { "overloads": [ [ @@ -1400,24 +1406,24 @@ ] ] }, - "createFramebuffer": { + "degrees": { "overloads": [ [ - "Object?" + "Number" ] ] }, - "degrees": { + "rotateX": { "overloads": [ [ "Number" ] ] }, - "rotateX": { + "createFramebuffer": { "overloads": [ [ - "Number" + "Object?" ] ] }, @@ -1477,59 +1483,59 @@ [] ] }, - "directionalLight": { + "background": { "overloads": [ + [ + "p5.Color" + ], + [ + "String", + "Number?" + ], [ "Number", - "Number", - "Number", - "Number", - "Number", - "Number" + "Number?" ], [ "Number", "Number", "Number", - "p5.Vector" + "Number?" ], [ - "p5.Color|Number[]|String", - "Number", - "Number", - "Number" + "Number[]" ], [ - "p5.Color|Number[]|String", - "p5.Vector" + "p5.Image", + "Number?" ] ] }, - "background": { + "directionalLight": { "overloads": [ - [ - "p5.Color" - ], - [ - "String", - "Number?" - ], [ "Number", - "Number?" + "Number", + "Number", + "Number", + "Number", + "Number" ], [ "Number", "Number", "Number", - "Number?" + "p5.Vector" ], [ - "Number[]" + "p5.Color|Number[]|String", + "Number", + "Number", + "Number" ], [ - "p5.Image", - "Number?" + "p5.Color|Number[]|String", + "p5.Vector" ] ] }, @@ -1541,10 +1547,10 @@ ] ] }, - "clearDepth": { + "green": { "overloads": [ [ - "Number?" + "p5.Color|Number[]|String" ] ] }, @@ -1555,13 +1561,6 @@ ] ] }, - "green": { - "overloads": [ - [ - "p5.Color|Number[]|String" - ] - ] - }, "box": { "overloads": [ [ @@ -1573,6 +1572,13 @@ ] ] }, + "clearDepth": { + "overloads": [ + [ + "Number?" + ] + ] + }, "max": { "overloads": [ [ @@ -1815,22 +1821,22 @@ ] ] }, - "unchar": { + "angleMode": { "overloads": [ [ - "String" + "RADIANS|DEGREES" ], - [ - "String[]" - ] + [] ] }, - "angleMode": { + "unchar": { "overloads": [ [ - "RADIANS|DEGREES" + "String" ], - [] + [ + "String[]" + ] ] }, "sphere": { @@ -2319,24 +2325,24 @@ ] ] }, - "shader": { + "mouseMoved": { "overloads": [ [ - "p5.Shader" + "MouseEvent?" ] ] }, - "mouseMoved": { + "mouseDragged": { "overloads": [ [ "MouseEvent?" ] ] }, - "mouseDragged": { + "shader": { "overloads": [ [ - "MouseEvent?" + "p5.Shader" ] ] }, @@ -2646,11 +2652,6 @@ ] ] }, - "noTint": { - "overloads": [ - [] - ] - }, "fill": { "overloads": [ [ @@ -2675,6 +2676,11 @@ [] ] }, + "noTint": { + "overloads": [ + [] + ] + }, "triangle": { "overloads": [ [ @@ -3054,14 +3060,6 @@ ] ] }, - "bezierOrder": { - "overloads": [ - [ - "Number" - ], - [] - ] - }, "doubleClicked": { "overloads": [ [ @@ -3163,6 +3161,14 @@ [] ] }, + "bezierOrder": { + "overloads": [ + [ + "Number" + ], + [] + ] + }, "exitPointerLock": { "overloads": [ [] @@ -3190,32 +3196,6 @@ ] ] }, - "splineVertex": { - "overloads": [ - [ - "Number", - "Number" - ], - [ - "Number", - "Number", - "Number?" - ], - [ - "Number", - "Number", - "Number?", - "Number?" - ], - [ - "Number", - "Number", - "Number", - "Number?", - "Number?" - ] - ] - }, "saveTable": { "overloads": [ [ @@ -3250,6 +3230,14 @@ ] ] }, + "blendMode": { + "overloads": [ + [ + "BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|REMOVE|SUBTRACT" + ], + [] + ] + }, "camera": { "overloads": [ [ @@ -3265,14 +3253,6 @@ ] ] }, - "blendMode": { - "overloads": [ - [ - "BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|REMOVE|SUBTRACT" - ], - [] - ] - }, "createStorage": { "overloads": [ [ @@ -3297,14 +3277,29 @@ ] ] }, - "splineProperty": { + "splineVertex": { "overloads": [ [ - "String", - null + "Number", + "Number" ], [ - "String" + "Number", + "Number", + "Number?" + ], + [ + "Number", + "Number", + "Number?", + "Number?" + ], + [ + "Number", + "Number", + "Number", + "Number?", + "Number?" ] ] }, @@ -3332,13 +3327,6 @@ [] ] }, - "splineProperties": { - "overloads": [ - [ - "Object" - ] - ] - }, "buildComputeShader": { "overloads": [ [ @@ -3371,38 +3359,28 @@ ] ] }, - "texture": { + "splineProperty": { "overloads": [ [ - "p5.Image|p5.MediaElement|p5.Graphics|p5.Texture|p5.Framebuffer|p5.FramebufferTexture" + "String", + null + ], + [ + "String" ] ] }, - "compute": { + "texture": { "overloads": [ [ - "p5.Shader", - "Number", - "Number?", - "Number?" + "p5.Image|p5.MediaElement|p5.Graphics|p5.Texture|p5.Framebuffer|p5.FramebufferTexture" ] ] }, - "vertex": { + "compute": { "overloads": [ [ - "Number", - "Number" - ], - [ - "Number", - "Number", - "Number?", - "Number?" - ], - [ - "Number", - "Number", + "p5.Shader", "Number", "Number?", "Number?" @@ -3428,12 +3406,14 @@ ] ] }, - "createCamera": { + "splineProperties": { "overloads": [ - [] + [ + "Object" + ] ] }, - "beginContour": { + "createCamera": { "overloads": [ [] ] @@ -3453,13 +3433,32 @@ ] ] }, - "endContour": { + "vertex": { "overloads": [ [ - "OPEN|CLOSE?" + "Number", + "Number" + ], + [ + "Number", + "Number", + "Number?", + "Number?" + ], + [ + "Number", + "Number", + "Number", + "Number?", + "Number?" ] ] }, + "beginContour": { + "overloads": [ + [] + ] + }, "textureWrap": { "overloads": [ [ @@ -3469,6 +3468,13 @@ [] ] }, + "endContour": { + "overloads": [ + [ + "OPEN|CLOSE?" + ] + ] + }, "normalMaterial": { "overloads": [ [] diff --git a/src/core/rendering.js b/src/core/rendering.js index 6a3346f316..984837df61 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -45,10 +45,12 @@ function rendering(p5, fn){ * system variable to check what version is being used, or call * `setAttributes({ version: 1 })` to create a WebGL1 context. * + * Note: In WebGPU mode, you must `await` this function. + * * @method createCanvas * @param {Number} [width] width of the canvas. Defaults to 100. * @param {Number} [height] height of the canvas. Defaults to 100. - * @param {(P2D|WEBGL|P2DHDR|WEBGPU)} [renderer] either P2D, WEBGL, or WEBGPU. Defaults to `P2D`. + * @param {(P2D|WEBGL|P2DHDR)} [renderer] either P2D or WEBGL. Defaults to `P2D`. * @param {HTMLCanvasElement} [canvas] existing canvas element that should be used for the sketch. * @return {p5.Renderer} new `p5.Renderer` that holds the canvas. * @@ -106,6 +108,14 @@ function rendering(p5, fn){ * describe('A diagonal line drawn from top-left to bottom-right on a gray background.'); * } */ + /** + * @method createCanvas + * @param {Number} width + * @param {Number} height + * @param {WEBGPU} renderer + * @param {HTMLCanvasElement} [canvas] + * @return {Promise} + */ /** * @method createCanvas * @param {Number} [width] diff --git a/test/types/webgpu.ts b/test/types/webgpu.ts new file mode 100644 index 0000000000..7c9016cdf1 --- /dev/null +++ b/test/types/webgpu.ts @@ -0,0 +1,9 @@ +import p5 from '../../types/global' + +async function setup() { + const renderer: p5.Renderer = await createCanvas(100, 100, WEBGPU); + background(0); + fill(255); + noStroke(); + circle(0, 0, 50); +} From 6195a956691b471db642ae1238ce085d14cff3be Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 19 Apr 2026 13:26:48 -0400 Subject: [PATCH 170/250] Update how we sample textures in WebGPU so we can do it in conditionals --- src/webgpu/strands_wgslBackend.js | 23 ++++++++++++---- test/unit/visual/cases/webgpu.js | 25 ++++++++++++++++++ .../000.png | Bin 0 -> 368 bytes .../metadata.json | 3 +++ 4 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/filter shaders can sample a texture inside a conditional branch/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/filter shaders can sample a texture inside a conditional branch/metadata.json diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index 7cee38e910..64f82bfcb4 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -584,12 +584,25 @@ export const wgslBackend = { const samplerVariable = build.variableNode(strandsContext, { baseType: BaseType.SAMPLER, dimension: 1 }, samplerIdentifier); const samplerNode = createStrandsNode(samplerVariable.id, samplerVariable.dimension, strandsContext); - // Create the augmented args: [texture, sampler, coords] - const augmentedArgs = [textureArg, samplerNode, coordsArg]; - - const { id, dimension } = build.functionCallNode(strandsContext, 'textureSample', augmentedArgs, { + // Create a LOD literal node (0.0) so we can use textureSampleLevel instead + // of textureSample. textureSample doesn't let you use uniform values in control + // flow, whereas textureSampleLevel does. While we don't have mipmaps, we don't + // miss out. + // TODO: if we *do* add mipmap support, update this logic -- we'd need to hoist + // the texture lookup out of the control flow. + const lodLiteral = build.scalarLiteralNode( + strandsContext, + { dimension: 1, baseType: BaseType.FLOAT }, + 0.0 + ); + const lodNode = createStrandsNode(lodLiteral.id, lodLiteral.dimension, strandsContext); + + // Create the augmented args: [texture, sampler, coords, lod] + const augmentedArgs = [textureArg, samplerNode, coordsArg, lodNode]; + + const { id, dimension } = build.functionCallNode(strandsContext, 'textureSampleLevel', augmentedArgs, { overloads: [{ - params: [DataType.sampler2D, DataType.sampler, DataType.float2], + params: [DataType.sampler2D, DataType.sampler, DataType.float2, DataType.float1], returnType: DataType.float4 }] }); diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 0d1e1e6249..235ccb7b23 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -273,6 +273,31 @@ visualSuite("WebGPU", function () { await screenshot(); }); + visualTest('filter shaders can sample a texture inside a conditional branch', async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + p5.background(255); + p5.noStroke(); + p5.fill(0); + p5.circle(0, 0, 20); + // This shader only samples the texture for pixels in the left half of the + // canvas, exercising getTexture() inside a non-uniform conditional + const conditionalInvert = p5.buildFilterShader(({ p5 }) => { + p5.filterColor.begin(); + if (p5.filterColor.texCoord.x < 0.5) { + const col = p5.getTexture( + p5.filterColor.canvasContent, + p5.filterColor.texCoord + ); + p5.filterColor.set([1 - col.rgb, col.a]); + } else { + p5.filterColor.set([0, 0, 1, 1]); + } + p5.filterColor.end(); + }, { p5 }); + p5.filter(conditionalInvert); + await screenshot(); + }); + visualTest('instanceID in fragment hook colors instances (WebGPU)', async function(p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); const numInstances = 4; diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/filter shaders can sample a texture inside a conditional branch/000.png b/test/unit/visual/screenshots/WebGPU/Shaders/filter shaders can sample a texture inside a conditional branch/000.png new file mode 100644 index 0000000000000000000000000000000000000000..2a307cdf649ba87c3084077ee525dd3076df7381 GIT binary patch literal 368 zcmV-$0gwKPP)aj9XT^(s5#mF>GVcWJV2fp4(o6pBK1<3($kK`|7 z-ZukTmgO?gbMtvIO+WyVQp)Fczc+(;fIw?zkjw<;4T}i`Hiqr|@nRFmW7g2WCJ-;j z3Jsb-Fu+R$0u%eb Date: Sun, 19 Apr 2026 13:52:32 -0400 Subject: [PATCH 171/250] Remove canvas style attribute update when changing font weight --- src/type/textCore.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/type/textCore.js b/src/type/textCore.js index 079294b64a..b978fdee12 100644 --- a/src/type/textCore.js +++ b/src/type/textCore.js @@ -1587,13 +1587,6 @@ function textCore(p5, fn) { if (typeof weight === 'number') { this.states.setValue('fontWeight', weight); this._applyTextProperties(); - - // Safari works without weight set in the canvas style attribute, and actually - // has buggy behavior if it is present, using the wrong weight when drawing - // multiple times with different weights - if (!p5.prototype._isSafari()) { - this._setCanvasStyleProperty('font-variation-settings', `"wght" ${weight}`); - } return; } // the getter From f181fd98123a941cd29dd951b81db69741889a1b Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Tue, 21 Apr 2026 04:15:47 +0530 Subject: [PATCH 172/250] feat(webgpu): preserve original types in StorageBuffer.read --- src/webgpu/p5.RendererWebGPU.js | 22 ++++++++++++++++++++-- test/unit/webgpu/p5.RendererWebGPU.js | 14 ++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index dc8da80468..02618426ad 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -3313,12 +3313,16 @@ ${hookUniformFields}} let maxEnd = 0; let maxAlign = 1; - const fields = entries.map(([name]) => { + const fields = entries.map(([name, value]) => { const el = elements[name]; maxEnd = Math.max(maxEnd, el.offsetEnd); // Alignment for scalars/vectors: <=4 -> 4, <=8 -> 8, else 16 const align = el.size <= 4 ? 4 : el.size <= 8 ? 8 : 16; maxAlign = Math.max(maxAlign, align); + // Track original JS type for reconstruction during readback + const kind = value?.isVector ? 'vector' + : value?.isColor ? 'color' + : undefined; return { name, baseType: el.baseType, @@ -3326,6 +3330,7 @@ ${hookUniformFields}} offset: el.offset, packInPlace: el.packInPlace ?? false, dim: el.size / 4, + kind, }; }); @@ -3388,7 +3393,20 @@ ${hookUniformFields}} if (n === 1) { item[field.name] = floatView[idx]; } else { - item[field.name] = Array.from(floatView.slice(idx, idx + n)); + const values = Array.from(floatView.slice(idx, idx + n)); + if (field.kind === 'vector') { + item[field.name] = this._pInst.createVector(...values); + } else if (field.kind === 'color') { + // Color was packed as normalized RGBA [0-1] via _getRGBA([1,1,1,1]) + // Scale back to the current colorMode range + const maxes = this.states.colorMaxes[this.states.colorMode]; + item[field.name] = this._pInst.color( + values[0] * maxes[0], values[1] * maxes[1], + values[2] * maxes[2], values[3] * maxes[3] + ); + } else { + item[field.name] = values; + } } } } diff --git a/test/unit/webgpu/p5.RendererWebGPU.js b/test/unit/webgpu/p5.RendererWebGPU.js index 0612d22e12..76a4e81a64 100644 --- a/test/unit/webgpu/p5.RendererWebGPU.js +++ b/test/unit/webgpu/p5.RendererWebGPU.js @@ -204,7 +204,7 @@ suite('WebGPU p5.RendererWebGPU', function() { } }); - test('reads back struct with vector fields', async function() { + test('reads back struct with vector fields as p5.Vector', async function() { const input = [ { position: myp5.createVector(1, 2), speed: 5.0 }, { position: myp5.createVector(3, 4), speed: 10.0 }, @@ -215,12 +215,14 @@ suite('WebGPU p5.RendererWebGPU', function() { expect(result).to.be.an('array'); expect(result.length).to.equal(2); - // Vector fields come back as plain arrays - expect(result[0].position[0]).to.be.closeTo(1, 0.001); - expect(result[0].position[1]).to.be.closeTo(2, 0.001); + // Vector fields come back as p5.Vector + expect(result[0].position.isVector).to.be.true; + expect(result[0].position.x).to.be.closeTo(1, 0.001); + expect(result[0].position.y).to.be.closeTo(2, 0.001); expect(result[0].speed).to.be.closeTo(5.0, 0.001); - expect(result[1].position[0]).to.be.closeTo(3, 0.001); - expect(result[1].position[1]).to.be.closeTo(4, 0.001); + expect(result[1].position.isVector).to.be.true; + expect(result[1].position.x).to.be.closeTo(3, 0.001); + expect(result[1].position.y).to.be.closeTo(4, 0.001); expect(result[1].speed).to.be.closeTo(10.0, 0.001); }); From bac9ea8fd73de5128b4064df53820163081eb14f Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 21 Apr 2026 17:10:46 -0400 Subject: [PATCH 173/250] Update RendererGL comments to reflect current state of pixel functions --- src/webgl/p5.RendererGL.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 63b72b30f4..78de4092f8 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -529,7 +529,7 @@ class RendererGL extends Renderer3D { /** * Loads the pixels data for this canvas into the pixels[] attribute. - * Note that updatePixels() and set() do not work. + * Note that set() does not work. * Any pixel manipulation must be done directly to the pixels[] array. * * @private From f393f04515300c145949518639fb81a47ffff5f0 Mon Sep 17 00:00:00 2001 From: Open Source Contributor Date: Tue, 21 Apr 2026 18:42:54 -0600 Subject: [PATCH 174/250] docs: remove broken beginGeometry/endGeometry links from buildGeometry reference The buildGeometry reference linked to beginGeometry and endGeometry, but these pages do not exist in the 2.0 docs and return 404. Closes processing/p5.js#8631 --- src/core/p5.Renderer3D.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 1fde0670bc..9a3ded2a73 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -477,10 +477,6 @@ export class Renderer3D extends Renderer { * combining them with `buildGeometry()` once and then drawing that will run * faster than repeatedly drawing the individual pieces. * - * One can also draw shapes directly between - * beginGeometry() and - * endGeometry() instead of using a callback - * function. * @param {Function} callback A function that draws shapes. * @returns {p5.Geometry} The model that was built from the callback function. */ From 41a0e50d6b3316bd80b8a005a56a3c5303bf76bf Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 20:21:38 +0530 Subject: [PATCH 175/250] use dependency injection pattern for friendly errors check --- src/core/p5.Renderer3D.js | 4 ++++ src/webgl/ShapeBuilder.js | 36 +++++++++++++++++++----------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 21d246eca0..e8eb0b3179 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -1997,6 +1997,10 @@ const webGPUAddonMessage = 'Add the WebGPU add-on to your project and pass WEBGP function renderer3D(p5, fn) { p5.Renderer3D = Renderer3D; + ShapeBuilder.prototype.friendlyErrorsDisabled = function() { + return Boolean(p5.disableFriendlyErrors); + }; + /** * Creates a `p5.StorageBuffer`, which is * a block of data that shaders can read from, and compute shaders diff --git a/src/webgl/ShapeBuilder.js b/src/webgl/ShapeBuilder.js index 6a2c723f78..9fbcf5955c 100644 --- a/src/webgl/ShapeBuilder.js +++ b/src/webgl/ShapeBuilder.js @@ -45,6 +45,10 @@ export class ShapeBuilder { this.bufferStrides = { ...INITIAL_BUFFER_STRIDES }; } + friendlyErrorsDisabled() { + return false; + } + constructFromContours(shape, contours) { if (this._useUserVertexProperties){ this._resetUserVertexProperties(); @@ -151,24 +155,22 @@ export class ShapeBuilder { const vertexCount = this.geometry.vertices.length; const MAX_SAFE_TESSELLATION_VERTICES = 50000; - if (vertexCount > MAX_SAFE_TESSELLATION_VERTICES) { - const p5Class = this.renderer._pInst.constructor; - if ( - !p5Class.disableFriendlyErrors && - !this.renderer._largeTessellationAcknowledged - ) { - const proceed = window.confirm( - '🌸 p5.js says:\n\n' + - `This shape has ${vertexCount} vertices. Tessellating shapes with this ` + - 'many vertices can be very slow and may cause your browser to become ' + - 'unresponsive.\n\n' + - 'Do you want to continue tessellating this shape?' - ); - if (!proceed) { - return; - } - this.renderer._largeTessellationAcknowledged = true; + if ( + vertexCount > MAX_SAFE_TESSELLATION_VERTICES && + !this.friendlyErrorsDisabled() && + !this.renderer._largeTessellationAcknowledged + ) { + const proceed = window.confirm( + '🌸 p5.js says:\n\n' + + `This shape has ${vertexCount} vertices. Tessellating shapes with this ` + + 'many vertices can be very slow and may cause your browser to become ' + + 'unresponsive.\n\n' + + 'Do you want to continue tessellating this shape?' + ); + if (!proceed) { + return; } + this.renderer._largeTessellationAcknowledged = true; } this.isProcessingVertices = true; From 324c592832b506b7f842973ac6074761c79cc951 Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Thu, 23 Apr 2026 15:25:59 +0530 Subject: [PATCH 176/250] Refactor: Deduplicate BinaryExpression and LogicalExpression transformation logic --- src/strands/strands_transpiler.js | 99 +++++++++++-------------------- 1 file changed, 33 insertions(+), 66 deletions(-) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index b30c3380e2..abf03ba3d6 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -235,6 +235,35 @@ function replaceReferences(node, tempVarMap) { internalReplaceReferences(node); } +// Shared handler for both BinaryExpression and LogicalExpression — +// both follow the same operator-to-method-call transformation pattern. +function transformBinaryOrLogical(node, state, ancestors) { + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { + return; + } + const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier']; + if (unsafeTypes.includes(node.left.type)) { + node.left = { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: '__p5.strandsNode', + }, + arguments: [node.left] + }; + } + node.type = 'CallExpression'; + node.callee = { + type: 'MemberExpression', + object: node.left, + property: { + type: 'Identifier', + name: replaceBinaryOperator(node.operator), + }, + }; + node.arguments = [node.right]; +} + const ASTCallbacks = { UnaryExpression(node, state, ancestors) { if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { @@ -509,72 +538,10 @@ const ASTCallbacks = { } } }, - BinaryExpression(node, state, ancestors) { - // Don't convert uniform default values to node methods, as - // they should be evaluated at runtime, not compiled. - if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { - return; - } - // If the left hand side of an expression is one of these types, - // we should construct a node from it. - const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier']; - if (unsafeTypes.includes(node.left.type)) { - const leftReplacementNode = { - type: 'CallExpression', - callee: { - type: 'Identifier', - name: '__p5.strandsNode', - }, - arguments: [node.left] - } - node.left = leftReplacementNode; - } - // Replace the binary operator with a call expression - // in other words a call to BaseNode.mult(), .div() etc. - node.type = 'CallExpression'; - node.callee = { - type: 'MemberExpression', - object: node.left, - property: { - type: 'Identifier', - name: replaceBinaryOperator(node.operator), - }, - }; - node.arguments = [node.right]; - }, - LogicalExpression(node, state, ancestors) { - // Don't convert uniform default values to node methods, as - // they should be evaluated at runtime, not compiled. - if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { - return; - } - // If the left hand side of an expression is one of these types, - // we should construct a node from it. - const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier']; - if (unsafeTypes.includes(node.left.type)) { - const leftReplacementNode = { - type: 'CallExpression', - callee: { - type: 'Identifier', - name: '__p5.strandsNode', - }, - arguments: [node.left] - } - node.left = leftReplacementNode; - } - // Replace the logical operator with a call expression - // in other words a call to BaseNode.or(), .and() etc. - node.type = 'CallExpression'; - node.callee = { - type: 'MemberExpression', - object: node.left, - property: { - type: 'Identifier', - name: replaceBinaryOperator(node.operator), - }, - }; - node.arguments = [node.right]; - }, + BinaryExpression: transformBinaryOrLogical, + LogicalExpression: transformBinaryOrLogical, + + ConditionalExpression(node, state, ancestors) { if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { return; From d52b6224b88c00e0e33be21f766d38cbd013b5d7 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 23 Apr 2026 14:23:56 -0400 Subject: [PATCH 177/250] Use more circular rounding for WebGL rect corners --- src/webgl/3d_primitives.js | 15 ++++++++++----- test/unit/visual/cases/webgl.js | 11 +++++++++++ .../rect() rounded into a circle/000.png | Bin 0 -> 612 bytes .../metadata.json | 3 +++ .../on a rect with rounded corners/000.png | Bin 5254 -> 5201 bytes 5 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGL/2D Shapes/rect() rounded into a circle/000.png create mode 100644 test/unit/visual/screenshots/WebGL/2D Shapes/rect() rounded into a circle/metadata.json diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 5eb78db6c6..0d10bfdf4b 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -1792,33 +1792,38 @@ function primitives3D(p5, fn){ const prevMode = this.states.textureMode; this.states.setValue('textureMode', constants.NORMAL); const prevOrder = this.bezierOrder(); - this.bezierOrder(2); + this.bezierOrder(3); this.beginShape(); const addUVs = (x, y) => [x, y, 0, (x - x1)/width, (y - y1)/height]; + const rr = 0.5523; // kappa: 4*(sqrt(2)-1)/3, handle ratio for cubic bezier circle approximation if (tr !== 0) { this.vertex(...addUVs(x2 - tr, y1)); - this.bezierVertex(...addUVs(x2, y1)); + this.bezierVertex(...addUVs(x2 - tr + tr * rr, y1)); + this.bezierVertex(...addUVs(x2, y1 + tr - tr * rr)); this.bezierVertex(...addUVs(x2, y1 + tr)); } else { this.vertex(...addUVs(x2, y1)); } if (br !== 0) { this.vertex(...addUVs(x2, y2 - br)); - this.bezierVertex(...addUVs(x2, y2)); + this.bezierVertex(...addUVs(x2, y2 - br + br * rr)); + this.bezierVertex(...addUVs(x2 - br + rr * br, y2)); this.bezierVertex(...addUVs(x2 - br, y2)); } else { this.vertex(...addUVs(x2, y2)); } if (bl !== 0) { this.vertex(...addUVs(x1 + bl, y2)); - this.bezierVertex(...addUVs(x1, y2)); + this.bezierVertex(...addUVs(x1 + bl - bl * rr, y2)); + this.bezierVertex(...addUVs(x1, y2 - bl + bl * rr)); this.bezierVertex(...addUVs(x1, y2 - bl)); } else { this.vertex(...addUVs(x1, y2)); } if (tl !== 0) { this.vertex(...addUVs(x1, y1 + tl)); - this.bezierVertex(...addUVs(x1, y1)); + this.bezierVertex(...addUVs(x1, y1 + tl - tl * rr)); + this.bezierVertex(...addUVs(x1 + tl - tl * rr, y1)); this.bezierVertex(...addUVs(x1 + tl, y1)); } else { this.vertex(...addUVs(x1, y1)); diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 0941c769cf..1c69ca83a3 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -1506,6 +1506,17 @@ visualSuite('WebGL', function() { }); }); + visualSuite('2D Shapes', function() { + visualTest('rect() rounded into a circle', function(p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + p5.background(255); + p5.noStroke(); + p5.fill('red'); + p5.rect(-20, -20, 40, 40, 20); + screenshot(); + }); + }); + visualSuite('3D Primitives', function() { visualTest('cylinder() renders correctly', function(p5, screenshot) { p5.createCanvas(100, 100, p5.WEBGL); diff --git a/test/unit/visual/screenshots/WebGL/2D Shapes/rect() rounded into a circle/000.png b/test/unit/visual/screenshots/WebGL/2D Shapes/rect() rounded into a circle/000.png new file mode 100644 index 0000000000000000000000000000000000000000..e5ec27fc3379fc0e9ef4cb5930f79562f92084ab GIT binary patch literal 612 zcmV-q0-ODbP)xQ`z|=w90WwO@pGmY_vpR@AzWMU6JCfJy#HtLUE0sukimRff3_?CdbFwL!bcZt! zQrUqhFm*0y%7eI1)!5XG%!Z{w+yVD&(uyzAAY)4BWM!^b8YB}uN9kCXG{{)3Ia%(> zJ_iSJPg1Y!e7zIN?B?r%m?crtAZE$nm7NC%S$XTXEvYVjBAMW1g+-ickVVAZ$S?!U zZrpO@K}fw{%gnNR@*pc-uY#sLh}cOz*D$iEqYP5mfnwE1ysT&`gGdmm=X34nr`o>` zqMabCg&r0_0%q0nbr6+!77Pifw=`6&4Mas$kkqd^Mm9r8P&zeXQz2zkex)0lzD@U y?Ajx94s#;E00030|EZ`g)&Kwi21!IgR09D1@>{Inw8j_!0000i5Ean@ap*LQO8`+( zQBe4B#(@Z^1A^m#5I~SUz$65cbUNvz_pYw0?&{j>)tp-qK1`Po=9}@GZ^qZ}_N{l{ zyZ4@R?*IJHxmV}^$n`fx{skE?fxh$q6E*&@cGt;@~2iT zXzf&G`TXgfXB<#lp7E#fLX^b{x^rdGawWoyX0t?mR{6la6lcTyo?g zogM9UogIhQcdGi>wu;Ub$Ik3r++N=~e@3vgzRdkpQcAGA^jt4RKYIW?V& zk7(e#SDeaM&tJ-_<7acmqC+`;Ry7M}RdM|6YUb29m^VGh$L6+hbaM&ktysvXmM=h* z;N-c@oH~Cx%a3hk@!|D|GMu)sjrMwzB}Y`VY{3jZfA%7-`n$#4bnywCfAXx3mYRT4 zMwJg$=))9I1LRL^uS2YhSdsqzej?Eb0|Nu>+_{rTBtklsqN1XLU@(Z=?Z(h`s;a8! z>+2(*SBnVgbehRntWf82IjJ+Wwa!Jyg=ur*^?FGp5~MR}CMPGE)l$Q?7krG%SItv0 ze5f)Xq)1_aFMe_nt4=r!T{FmLv((qu6BB{5TAs4%N;K0(Tm;zD+l$lXpd{cYXJs)B z8<|v^cx&inmzS|?rJpVe&ctPRuy^lX1a5rc z6AO_`DI-10#lk_cc1cMI%9>8+ehZF{j^gwAP|sy$Wf+FR;NW0k zRmR81sjaO=)$jIEzoVkq@^dd_G@2lj(}*Q9czi+pem~)GxX^#ko;_qTqPWcK@pv$^ z6D(}2R2t;fzhdPtDbgVdbj)h1A|lhyQ$o7L0LLD?h)7B&i-ya{Q&aB4M!H~q1w#c!nP&^(ig;8#Njo@6FuGZ$FapiD;o;8l4gmFqcbW%I{1zNub1!!|uk)S;AaC zSvgn1KaxTq%Ksc9gYreSG77p9XOtR>h*bSvDW*taAZ6i^krC4Aj7;Ptl}w{qdAu$+ zS)tOr43Q9jahY~T2ZwN(4o1hqcuUIo_$O8~@3<4$HyUD6*ec));0pwC3PkNz8m9&h zBS&a-gjjr>(m+5IwS&v#*}xOrYS|H z6n#pIG&lqzvB?QM;^1m9q7gie@ z=x20jSi0m1di~VZ)sWCNMq+8ktelLV$K$pU3lES_j8akV$29HOHJw;ARuDW;T7qBV zQ8t&sW=i~$I**0V@0D+DGRD+fM4Z~F0-tJCQCzJ?I2>VcC?dX+Bc94KM{r+WSH~OM z-r>mh!>RCj5XlIZ*r=-TF#FI$@Oj)+Rs@-hN2#f*pi1IQTq1UOJjOoB3S#j|EGtDK z9>XC%=kPhjub@N}NC{+>kEpp7oRlo8Ob8|0OjG@wQi>{~mR2ouK(Je0Q6^S`T-Gj= zrpTmom@=`yyh0*-EzxA2ZEtr|>i2=*Kb4rEr+Yh2m&wFLxB$Ib=^&#)ph66iOyMw`*o8nmZZDbu zRgsPQZ3*E}ltF*HqM`}XZPOc!sx^)`V( zS)m>t4$;%Ihmr6Y%3&L8>&Y5W)6@VC8%{CCytzk<<@eziA9T3flABrB^gL&uy@qI} zgoNS3uxZ%D5X!+7^`|7mQ1hwZ6uF8jl9A+4!K<>el1O+$_}xQOa|1TRK|G#fpntd! zz!oi9B=7tJnd(AmZJj0Xu&A!8$L4WiiRyi#)WlGT*zg!hSw*kgK{^o=s5uCDUE&vR z?4{M*^P3lFU$%~K-1Tof{NxKvBqmXTRtcUGC6w%C;%DiP0xe6}#}sR*PJy5YD+|v* zcRyvpQd(zJGuAi6bI&}>4L99{-DYvr+#}Il9_%79~C} zP4x}D^4cr(^zETeGUqMbd-%npFL6Z2YHt1UMlvF8CYR#mjuRkjq2R3?+%7b%Rz?L# z*FgMe$MK(-=bmmd{Mv<2D4D} zy}P?{J4_O>sN_&pLZ3m(gKn~+Jxp(I;=~o7;H^D_ytrkETYvHtSAP3nIseKHJpSA} z#4<@58yiH4oSJ5T$(sz0hEWk+DW(Ju$`(SU^WvDORAv8%fBwIyB5Hs>N%~Sri@6Jy z5|!j{^VaQzN4D|8(~r=*eY5OSywumYaC&ugTbi;mKRF3-rp<}7w2nLOe}dC5xsH`< zzrng2f5tgi+`#kO_K4Sjme(YW&kChyId}EB1*;>MGMo?>Gh3P&kyXxQ)AGHA3eH0# z!x%OLyRarjF1<)wVPJ2djF@amQ{hZ?KH;LF2Q*U*!i6{5vV#MLrj&wV@6}woF;g&ik*Q zuKyzS?kG)VN$$JlGMZ~ku*tU8C}2O&ic^oHUve*(>>ti{q4D0e70CW|ngL!d8xJDdp2kj^I}} zUBdcH&qOy&ZvW980y)scmGgk6X=s*)&ygl5dl|0**>Eq{UvMHXZ@ig1H(bNUA8+7O zOBd2u(@JP8OHgRs?G6Y8EkYxsAi>a-m7>TcB28_bF|)NWu@c-g%|c3DU0q!WlFHfA zviVUPEmb08V`OC)nM=jVM7nAACoyHey>X+^snp8SnwA%=Yj|~wiN0M7?Ab+9VpU1a zG<@Zi1cEvZIEGSIV~H#hI#iO1hy-`+U+l2%fs(>P}7D)A>fr>t1RnX4}ql39*rIV6?@ z*u8tGpn#$41zC!STwIa39QQ<`VFD5pG7?q2;%KRC4$ZU^ibNR~cv<446N!|ZZS7*~ z*6ozYv5w)cl1S}h{-I@j_iJCki-sX#(lm0MvY?IQ+iE!Z{B_hFxsaZ4lJS8_W*#<| zfTxCTq3CIc9!i!;9(!aXTedt$UiSEzbX*v*lwes631LywbW-dp3N4j!R##V{oXg>` zi)AcqrVVwxqxfAnY`A@X0>N_GM8!CywT)o)G_sm}7pTDN_HgVR4^KXP6Tf@lW*)uw zJ6wFm65f92Z48eD((^vU*YCQUh>Y{X=GXY+AD@>bG|aHGk1)sh9`tgXcd-e^W3Dg`;2d1tuG(3#U;bzMl+j#!j7iI6B zr?RR6r^}De>B4lBkTU}$(=J8^^y6_=It=cps7YN*978>6rsI`sAKrDxYJ-h6Wl;pn8KB^G^R z>As;6@f(;q>oCGX5IeSR#qG7zD4VW)EP@ zrhX@6e^Nw`;yz+Yu2m&xw@UDq=IJ<{USW`kdw%3-H#=V(YfOyz|D()HJqWmQ--snP+48 zt0}2!WFi^E5(}E%3L0cz?(_vXL?B}KRxuRKV)s_^;>)k$_5_4lHC~oEa0HmJz){{r z5<6!~<|C!g9xWFm!P!yj$qtpplmd z-a4~}NVFepZnnStJPVhtl$6DZWhHt0^%vQOO-@$zkK7uJq*o=gnc@1HSyw!du+4ud3 z@|CT-=oyTv)lfxd`SbfPDWbm8Zn^Dto_Y3pcBtb&kAF_BQ1D2)utqYwSM03EfN z^Ll-<3KrGXRr0Nkyd;|8q>aZmk04jk)sXh$^s8R{sVrW+WI3Cj_$|MD=zf_yM*Hm9 zv@ckOQyb@Jcik$Vjp4Kz*hulo)obXNGpuKR_gl0~obK13m#CbQcM0^#kn~T>;l+dq zmz0x}48hta%ybl|OR$s}k(gsAZ!g8nMTKle$fPH^Vf}Ud^V8d99u~{LqN^e8AF7Dz zxOmCOR&Ltx-8;B+&Dq?2%hf#gFSqc(?f*!2U>lFzb{!Aib`6{ExsjiL?*`0FjQ%Z8 zbJz9j=-c)Oet7ML-2JVuV5B2Fe9z64xHO*l<-Pp=m-nEh!gO7+nm}fd^%pHeOGYrn zFK$|UCeO+KX6<>a>AG=)l2em^N*1Y#-ya06cYCM-7VqjEUHqesn>Jm1?f1F-hM#fn zmEULG_4jesRd;gvt&ecZx|_MM>ps@q@*6JS@DP{Zw2^#_>(_CM#P z&C1iI>cV?@7C?0gF?Ly&)g@zgWpjmEK}ZR&%6ggapze>N$omIWN~xkI`tL4k-1pCQ zP{04HiX7D7f6b5nrpRB@+6Ob&e*ypi|Nn_E=pq0B00v1!K~w_(E4}C4<%;D{00000 LNkvXXu0mjfLHIcT literal 5254 zcmV;16nX23P)4XvDkHYFtuj?>XWH7)E{3?&sY~r{ ztF3h?7!&uL$=u%UhF>`42vmg@GVn$R;%wZUhE{KHLwlH2z_{}d24 ziM3LQI(>d!d&h^DarL^B_`*k4vhK9wIRBKnoHnnP<@0Jd?YLUz*ST2I9N}*k&*a1z zAue09oJ&_PLqKr$;u)N~w3*c>x3J>)1_TV}EpKIEgUQN8wX9k;n+sPg;&Xp{23MT7 zi1y2tv2o1_Z8K^^#{}rTfG7~ptd<3Ys|a^(mh|`c6OYFk7#LvRzJ2!Zd@e_Ibv2Pl z1h3bNq3hJt)X>-0hhKgSb5|Lg65#auL&TP{`#;rzY&N`CP&7%Y{72c%0Q2UP)kjE56EVEUiSjWXat1 zI1F6?&cW?;ap=$?a@j1amd;jSkV8pFx}(o`1JX9DzM}2acU~g?NBI$;-=gE zHU~#XM+pQ1sQb#wN({qbaB$F;%J}#=_4W0r{_q&}J0ZYUUvfF43E_T8Bb6!O3q%M8 zgT!Jndwfq%k1ZLsu98f5cQFHBH0mbmH-N2#3SAiYXuthr=GDRK@4_p;XL}XNpLVFyQ2qPa&SuDWc&qEb6KP zILO=FS8AwKR52!*Oxj?g(WtGmYAyAicRBP)o+~|U6VGH5n#*S?TP3_c4=K@cMUYxZ zEueZ`0EyTzbe%+eob*J7Vxfegl^Ksk>Ds%8(dY;sw}WzVf{B7GS#aVBx$*cz>^pFf zT)s%8vVwdeM?4X?*HH{KP1Cj;OYBUoweE}sirjW}IkM?TK2Z0fR`z7+blL`E$-~1^ zP{yoOq^hb4^#FzS#e^CZv-!hgQXoo2lsdUYwM>Ue{=d{q z({$TLJZ=xUOcu3BTvV<~JXWDtz~yoZcXJGnj^T6%IqRJBSh(;6#*+zqTppc}5LhZ@ zG39rmm?azv;&OT^m&yVlfKt_hcrK;F%KltJgR({C(G*>ZVFKWofV4GKdPQ3-hKGmA z=L@osn_M=JCN|>tcqxi2wPd!8*o((>GCDYf$8<3|7Q-K^qEO>2%s^ zs%aWZGRkH&O*;yNdZ4SzkeX!4ioL4fHcQG%Q0bBK{y#r`D0tHAYf-#>ypG%EIS$kctgpWk#v43Syd0 zoT9d=M9L;SToEE9@u*lT;4meANpDdm5cJEp4w>WVQ%F4ep$dHJsfyxC8nIYh{CiAn zr9>uMWd4FhR85`A8@u1)goVdb9q=KP5eYe{srGT)+-U@SUZzw>m`Eq6tEr|&;!Iis zX>2@2uVe+O^aSN{j!Zg*OKi>+aEo1G15ie-Y(lNAG(yp$T2frHa_{Q*k!?~S%Ak~( z!=k%YQ>uhZP%1iQ(VY195~eI1tg4pCUQZ%xv3pM!6~O?A{^v4D4s>%f z3w9Z9oDQd`p&v~YH7m}>=k+6`G1)OG5VwHHi)R`d5^v-QU>F)6uZblS#-eee(HNSh z(b?IF%k=Q(n|la{EA4)GILd(oJ&eT0PzF1_zJa0vbxn=na^MzbELnV_aK4wI*r3bf zmE5e1L$|o-gBwT`LSzgNhC@TCkuq=<3UcBh)q023o+FM)fvAs_dQF)!g?KC}{@q8@ zj7A)Wi*!22K>rXqi7ls`a*DhQQe9PRx0aT9q8?>xYZ`F)Jd_1`pMXjZMM({hkrgTW zy)N>Zl&G4Eu-}8z?ZsJ9%LBi9iG}BGa7giDQ;LvMg>S^FY?(@r+(BE^*|I+4N?;*Potz04u#34v-IrmrCRc$ znkki>zjifY@mR6ZVFF$^B}bNu$}sVf2^wb1L8URtxCFguf;p|tM7ia+Km6nCq(GDe`XuSgWy>sHb|&MAB-?j&5*yjgi(7ul!Orb8PxaGK=fUmQ z(H(gzD}$6Iz?lv=?ux10^ON83{wr=|?UlE&@sanU+!CLCdEcB0-oLzsQ)gE2;gyT{*{xTw>8cAPaGHGUd-o{(XkyA1 zplKRfStH=e6OmfRZ$L42kefbw2CrJDR5XC~7 z=*TFPEs?0nWQUM+^yArc=GY7CLbxd+R>oFaTPrDj-Zr*;K8LCnRf~^}QIsmOluHZP zE@lL?m{Lz{-Yo7^dc|o?vqT{@{Cb&W-+l&q_LCL+2-VFZP&I{cL}yN|j>k&y=B}-D z?tPhTW{}a*LAty55v~Z>DyAETU4R`;F%qgLd1{DJ1w<0#@wgb5!w!Bm0z{R!DnKa_ ztEjDjsEl`bppPw2|CaHxK{1!&W9!cm!Y$OdrIG|8u>oB-xc*ZgV&Fh0JGSrTH@|*^ z-a~1uPz$+=S)6p{I?2=d#rn`H{24LvAP0J)CPYxt4wRt=d zBP=nYkjqLP5+qkFp_xu8UlWWc)0D+XlbM|CZS7~5c(jmg>loe|iPRpJ&aLDdU;2Ce zXksT$@f9V`S=P#Ft#zFJk&R3}VL1n4S;hw@n6qFpVP73x;-Y8Gol8Mt&6AJ+l%4g?w5(VujT7b&`3PF-%Ii2x2Wopq6 zj#Nnzm10^;E0Nk+6tyyra5a9fkCW&7c>0&O^3=n3@WhY4&d1MR$)30NVE81EUiJyT za^L;L4F@l6e~lMkd_j`X7{ijFKJoZ3Fl8Q>TFoKhIYWYeMV;2{p|-_MR6JBcMGBrPe^C*b>rM#OGl&b$T0#6j%cwF|G` z$#f~YtWt)qJv({h^;cPX(h^bSu&kfQbebqy90Dv94B5XkqAimlnN%VwAlAsbrn_*OJs4SABP^7=Ro5Dm2r^|^_L$B9EC{j;SGG)!wNy%&$vmi6~)FvEygYAUz&sU)NsT3XB+KRHAs$ zUrBw-90CGXcbn8V&0;({MulwoB*tSnMg97_b`z06C*_%1izy_wE|Br7kzf{U-Zo~*+|w&Y;l<)0?+ig4x! zHqg+zm^GJO&FquTk%&+`a;%h&-}Q3goT*{ZPsi5KrWh zs2-NUb=KOC;;);>uH6TDYsWTfr_aO;Rde127hwcz3Dry|nN5kYrZN51G)i6W4uqN3 zJO`)0hM`1J`YCLE8Tj}lGjmyw+=%zSNld1KMn5H2rHh?-j&XO3h15GxtF~;RPc<7PG5wpf& zWMt24h;j*Pf^G`wVJs`lX!jce-cQ#+LQSznE~-iQ-{-plv7@pQr7~Vk0$WMSO-s{c z=R>DbaZmD}?K@sa;-s_!MGG35X3Cey*c>%ds~2P!(JdK*B-3b`X0NJ>CB%QJyYaEO zj4kn_dw(EfN_ae8R63;VI!cu$Qd#-ZkZ4}C|7rB(i=AqY4s|)Qy&I5CPrtN>yT9>W zbi;vie;OLPWU^|*dUi_-nrsk!^olESm;p-iXGscDKYbSS=FcZ3@u20nh3tIoWi;I( zL>5uI_@z=AT~tH?6AHx>ft z-ykV?55IfrU(pI_x?X=lqH<2&WzZ+0GCnVx7fB&5D_UD1Qs0D`PvG{5E@eg}<~Wg^ z2+UGK_MJxrXd11U;;HT3yxxxNHq|DD(K z!{7Qc#ev;Ce)o+$a`z2vdEgd){LPy&3n}_{KFfVKZKQAa@A>W*FX#TRejXzq z=a&!MLCB-=o5y~{Zy$RAEf=HX+VzABgKYZPDzt1IJw3v$S6;wG{~k78wx0P-Q`y!f zH4&^30E#B*jy~TDi27O$T+!J*xZ-;cJ-6k$J0Io~U;H-L-25PyUiTd~-t-U`e(qkb zx$AMx*?0$+cRa+#JAcJBw?E1?w{GSi?|GVQzxpT}zjQAfuKN}j|J^3mZumTFFa0d% zZ}A@{L@ca<3bf-FY59T_A5KWq*MY_q) zKLzCQC^d=V$O=n1BRamKP$+j4EvutIxub;DA?n{@$$i->cNC$cV3j(`_Pe+5Wn4$u zqC@red-+BbszU;ON4Z??kU2YwrMLUCWpiH`YVvX1lQkd&Hn@d0RR6#e_b;G000I_L_t&o0B0B#?3}M2TL1t6 M07*qoM6N<$f*ANaivR!s From cb0e184738652c40dc0b7fe08a6577f7a8f121cf Mon Sep 17 00:00:00 2001 From: Perminder Singh Date: Sun, 26 Apr 2026 03:37:52 +0530 Subject: [PATCH 178/250] moving random to strands-backend to maintain consistency --- src/image/filterRenderer2D.js | 1 - src/strands/strands_api.js | 8 ++--- src/webgl/p5.RendererGL.js | 15 --------- src/webgl/strands_glslBackend.js | 8 +++++ src/webgpu/p5.RendererWebGPU.js | 30 ++++-------------- src/webgpu/strands_wgslBackend.js | 12 +++++++ test/unit/visual/cases/webgl.js | 17 ++++++++++ test/unit/visual/cases/webgpu.js | 17 ++++++++++ .../random() colors a basic shader/000.png | Bin 0 -> 4826 bytes .../metadata.json | 3 ++ .../000.png | Bin 0 -> 4818 bytes .../metadata.json | 3 ++ 12 files changed, 70 insertions(+), 44 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/random() colors a basic shader/000.png create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/random() colors a basic shader/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/random() colors a basic shader (WebGPU)/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/random() colors a basic shader (WebGPU)/metadata.json diff --git a/src/image/filterRenderer2D.js b/src/image/filterRenderer2D.js index 2ef476050b..40f0c52685 100644 --- a/src/image/filterRenderer2D.js +++ b/src/image/filterRenderer2D.js @@ -17,7 +17,6 @@ import filterBaseVert from '../webgl/shaders/filters/base.vert'; import webgl2CompatibilityShader from '../webgl/shaders/webgl2Compatibility.glsl'; import { glslBackend } from '../webgl/strands_glslBackend'; import { getShaderHookTypes } from '../webgl/shaderHookUtils'; -import noiseGLSL from '../webgl/shaders/functions/noise3DGLSL.glsl'; import randomGLSL from '../webgl/shaders/functions/randomGLSL.glsl'; import randomVertGLSL from '../webgl/shaders/functions/randomVertGLSL.glsl'; import { makeFilterShader } from '../core/filterShaders'; diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 19f7e3bac3..d7d199aa97 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -399,14 +399,14 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return originalRandom.apply(this, args); } - const randomVertSnippet = this._renderer.getRandomVertexShaderSnippet(); - const randomFragSnippet = this._renderer.getRandomFragmentShaderSnippet(); + const randomVertSnippet = strandsContext.backend.getRandomVertexShaderSnippet(); + const randomFragSnippet = strandsContext.backend.getRandomFragmentShaderSnippet(); strandsContext.vertexDeclarations.add(randomVertSnippet); strandsContext.fragmentDeclarations.add(randomFragSnippet); - if (this._renderer.getRandomComputeShaderSnippet) { - const randomComputeSnippet = this._renderer.getRandomComputeShaderSnippet(); + if (strandsContext.backend.getRandomComputeShaderSnippet) { + const randomComputeSnippet = strandsContext.backend.getRandomComputeShaderSnippet(); strandsContext.computeDeclarations.add(randomComputeSnippet); } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index a9b400fef3..78de4092f8 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -19,9 +19,6 @@ import { Image } from '../image/p5.Image'; import { glslBackend } from './strands_glslBackend'; import { TypeInfoFromGLSLName } from '../strands/ir_types.js'; import { getShaderHookTypes } from './shaderHookUtils'; -import noiseGLSL from './shaders/functions/noise3DGLSL.glsl'; -import randomGLSL from './shaders/functions/randomGLSL.glsl'; -import randomVertGLSL from './shaders/functions/randomVertGLSL.glsl'; import filterBaseVert from "./shaders/filters/base.vert"; import lightingShader from "./shaders/lighting.glsl"; @@ -1907,18 +1904,6 @@ class RendererGL extends Renderer3D { } } - getNoiseShaderSnippet() { - return noiseGLSL; - } - - getRandomFragmentShaderSnippet() { - return randomGLSL; - } - - getRandomVertexShaderSnippet() { - return randomVertGLSL; - } - } function rendererGL(p5, fn) { diff --git a/src/webgl/strands_glslBackend.js b/src/webgl/strands_glslBackend.js index d0715f53d4..9ec04b4fae 100644 --- a/src/webgl/strands_glslBackend.js +++ b/src/webgl/strands_glslBackend.js @@ -1,4 +1,6 @@ import noiseGLSL from './shaders/functions/noise3DGLSL.glsl'; +import randomGLSL from './shaders/functions/randomGLSL.glsl'; +import randomVertGLSL from './shaders/functions/randomVertGLSL.glsl'; import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType, DataType, INSTANCE_ID_VARYING_NAME } from "../strands/ir_types"; import { getNodeDataFromID, extractNodeTypeInfo } from "../strands/ir_dag"; import * as FES from '../strands/strands_FES'; @@ -173,6 +175,12 @@ export const glslBackend = { getNoiseShaderSnippet() { return noiseGLSL; }, + getRandomFragmentShaderSnippet() { + return randomGLSL; + }, + getRandomVertexShaderSnippet() { + return randomVertGLSL; + }, getTypeName(baseType, dimension) { const primitiveTypeName = TypeNames[baseType + dimension] if (!primitiveTypeName) { diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index a122acaeb0..f20300a5da 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -14,10 +14,6 @@ import { materialVertexShader, materialFragmentShader } from './shaders/material import { fontVertexShader, fontFragmentShader } from './shaders/font'; import { blitVertexShader, blitFragmentShader } from './shaders/blit'; import { wgslBackend } from './strands_wgslBackend'; -import noiseWGSL from './shaders/functions/noise3DWGSL'; -import randomWGSL from './shaders/functions/randomWGSL'; -import randomVertWGSL from './shaders/functions/randomVertWGSL'; -import randomComputeWGSL from './shaders/functions/randomComputeWGSL'; import { baseFilterVertexShader, baseFilterFragmentShader } from './shaders/filters/base'; import { imageLightVertexShader, imageLightDiffusedFragmentShader, imageLightSpecularFragmentShader } from './shaders/imageLight'; @@ -2745,10 +2741,9 @@ ${hookUniformFields}} initStatements += ` ${varName} = INPUT_VAR.${varName};\n`; } - // Find the input parameter name from the main function signature (anchored to start) - const inputMatch = main.match(/fn main\s*\((\w+):\s*\w+\)/); - if (inputMatch) { - const inputVarName = inputMatch[1]; + const mainStructParam = getMainStructParameter(main); + if (mainStructParam) { + const inputVarName = mainStructParam.inputName; initStatements = initStatements.replace(/INPUT_VAR/g, inputVarName); // Insert after the main function parameter but before any other code (anchored to start) postMain = initStatements + postMain; @@ -2792,9 +2787,9 @@ ${hookUniformFields}} // Add private global preMain += `var ${declaration};\n`; // Initialize from input struct at start of main() - const inputMatch = main.match(/fn main\s*\((\w+):\s*\w+\)/); - if (inputMatch) { - const inputVarName = inputMatch[1]; + const mainStructParam = getMainStructParameter(main); + if (mainStructParam) { + const inputVarName = mainStructParam.inputName; postMain = `\n ${varName} = ${inputVarName}.${varName};\n` + postMain; } } @@ -3757,19 +3752,6 @@ ${hookUniformFields}} } - getRandomFragmentShaderSnippet() { - return randomWGSL; - } - - getRandomVertexShaderSnippet() { - return randomVertWGSL; - } - - getRandomComputeShaderSnippet() { - return randomComputeWGSL; - } - - baseFilterShader() { if (!this._baseFilterShader) { this._baseFilterShader = new Shader( diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index 612fcf748a..59aedde92c 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -1,4 +1,7 @@ import noiseWGSL from './shaders/functions/noise3DWGSL.js'; +import randomWGSL from './shaders/functions/randomWGSL'; +import randomVertWGSL from './shaders/functions/randomVertWGSL'; +import randomComputeWGSL from './shaders/functions/randomComputeWGSL'; import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType, DataType, INSTANCE_ID_VARYING_NAME } from "../strands/ir_types"; import { getNodeDataFromID, extractNodeTypeInfo } from "../strands/ir_dag"; import * as FES from '../strands/strands_FES'; @@ -267,6 +270,15 @@ export const wgslBackend = { getNoiseShaderSnippet() { return noiseWGSL; }, + getRandomFragmentShaderSnippet() { + return randomWGSL; + }, + getRandomVertexShaderSnippet() { + return randomVertWGSL; + }, + getRandomComputeShaderSnippet() { + return randomComputeWGSL; + }, generateHookUniformKey(name, typeInfo) { // For sampler2D types, we don't add them to the uniform struct, diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 1c69ca83a3..6e05ddc06f 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -1076,6 +1076,23 @@ visualSuite('WebGL', function() { screenshot(); }); + visualTest('random() colors a basic shader', (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const shader = p5.baseColorShader().modify(() => { + p5.randomSeed(12); + p5.getFinalColor((color) => { + const value = p5.random(0.2, 0.9); + color = [value, value, value, 1]; + return color; + }); + }, { p5 }); + p5.background(0); + p5.noStroke(); + p5.shader(shader); + p5.plane(50, 50); + screenshot(); + }); + visualTest('uses width/height in getFinalColor', (p5, screenshot) => { let firstShader; function firstShaderCallback() { diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 235ccb7b23..50d696d434 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -325,6 +325,23 @@ visualSuite("WebGPU", function () { p5.model(obj, numInstances); await screenshot(); }); + + visualTest('random() colors a basic shader (WebGPU)', async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + const shader = p5.baseColorShader().modify(() => { + p5.randomSeed(12); + p5.getFinalColor((color) => { + const value = p5.random(0.2, 0.9); + color = [value, value, value, 1]; + return color; + }); + }, { p5 }); + p5.background(0); + p5.noStroke(); + p5.shader(shader); + p5.plane(50, 50); + await screenshot(); + }); }); diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/random() colors a basic shader/000.png b/test/unit/visual/screenshots/WebGL/p5.strands/random() colors a basic shader/000.png new file mode 100644 index 0000000000000000000000000000000000000000..92b7e4956a9b7746be6e1be6b8dbec381b0a1329 GIT binary patch literal 4826 zcmV<05+&`4P)MiqT z?(IH(&gs6B`oIGZY)(4qq-N!nS8g`lbkpXkr=Duwd+)vG!V52K?zrQQX5W4HZQ9z} z8u$J5(@)KI+ilmp|Ni?;OG``B(b3VYw9-n=1jwUfXD+jha4v`ZNa}bWn5OefKq2TyaJ7*=L{S zde&ce-F2I@&ptbUYj1CF)>><==9gc7Y0f7`?Z6;_CY4?Z{+UU=b{X{MRtj5E%NFTVI9`t|FV*GG;V8C6xq3opD7J$v?yk3RY+ z7GHevyf)o*)5Q%p+z`9%wp*Ng?zyqlQcFeIJVuWm9cP_&R{Z$mkFnr_3&yX%{u(90 z=+dQ2^ytwe#*7&gCHTC)#1c!yz<~qf_uqeynP;9kjydL-*m1`lW2&j9in->RD|+?n z6~`WXY_7BIw%bNs*HIFS@#DwG2OoS8v(G+z)We1i%L`XsbyY01&_Z$9WtWA(|NQgM zeDLqT|BkU^$7W&@u~|VNcGzKuTnqpb7tFHDE*lR&{BR}-oOj-NC+@lDo;c-{Q}P3- zJ@?!*x^?RoE3UX=oPYlL368z)y6djE^Ugctk3as1=bwK*lk8HGmhdvmEEB_r508lx zC&qQxT^Gk4cU*oqO%tuHtrufP6!6k5fcbIzG@s)T3PSYwT- zA%hkIopsh(W7l1G4QSqYm~X!Ml0LM99<=3t&*C^*;5f5EUzAoM z^t}H1>(dlGt2(rcB_u$TfCWmgUcCZ+i^D}dV8DPFP(Jv{4e(y6zWnmbm}j1OQuwpX zGD|%8;DfQ=dh128)iee7J@?#mDLQoWyn7Z~Y_Y5$7g}@8HA97p{bo{0q;=0Z=bY5cYp=Z)7-P>GD#gz;K;>e)Txy?v z_6Z+Iee%gCDf-n{Up;8)f(tH4`y^gLLJUV};ee=LC*t*+t3iY z3I6D#k4`j+bjXk)Y3)Ej9aKRr2K4Etp9b@x4S=Z5rI%isdq4WkMX+NhZlaZ6Xl;q(_e)X@~eJneV^X{!tUE{(vGH zosOw7;Ugm;O34Y<0&H1{*|g3L*=eSkCcVAW|GM7sxlmm%lRB}#1W}g29g!AI6lf$@mcHM9`=z}L0w}63h)y6;PlM*7 zo)ruIz542_!M()SQhEs%0Zlv5hvZPxgYx@sYikQmfJ&^6ZR)NZiAxcxB-vtBRq1LF z148Ne4Qn-l7_W#P6Lg@SOz` zEGN<3E#w(Nqo2j~A0K~P8%^{Pnv#@LA-iVB%P+rtxGp%=pc~gQLS>nbIOs)!7-Q}K z&R#FR_+pqQYScOJyz|m3bzch{=Uk33D*?2r%Z z0m`k6f+lv<&{sUf3XLPI)`3ZYY>zk%ZK|g##%g`W8G-M&x3}jBl5ioLNr;Jdaa=)_~v@(4gjubZMrhP0J&37IsG~ZK*?-GO#B_aWbaI(I!wS~PJR~R zUT7joG!P&vpp+oF8WS-!Sf`_-BaZ}{r=$s;_3QfOZA%z6Ot#iSMb6||V00}9J|J;M zZ9zw8IfegpvS}0*s@^Nel}KzUz)39aRA z2&3Unk^}%Yuwj(a0IlugZ}&SIb3zoX1!PlH;L~0zK#&QrKWGW8s;Yb))4hB5JSSL8 z^FuGLB|%i;G61>u+H09zpa{pa29xkN8<(6Z*?pc>8A7+dai;SqAR!j4(uT`!_JirLV(!d1}8CSKoINIx(>pcmwDm}Vf4v#RN%TccU$XktAgl^Fy9 zr;4=W9@g(~0Wl6BnWF9K8GA7m)MqfNDNY*WZr=9PZh#=VIFYecomr>lD72yza}o4& zpn{~btDa$wEfTP%sN}e9Z0KJOJgVk?qVx>z0|#YPkV&AMk2nPF8qF{SO;*x0p^vGk z=>YI_V=c~D;`$Z1wchPY?8fvS1{kN~)$$(1bZjmsKV1W&Xll11BfZ zGf)7942-HTP>8cyNcym8wu*i#s`}M zEitodLPsee=_y7e0IOV1zxeDSNTPSzf zB%x9`k8Y5pLzVVnvsLx~W3$SmD9U(Q;dy5l{Y)hRz{16AEpI;2O+vNI=t83roQwqy zC47Xsb2JyxJZZyhrsQ;_Pk8OAsY2H@YeU0iRaH5buxp^8rRPuyU@B|u(G=Z7kBk5l z*f*!rb5z9W4kXvuu8LWU!vQ0wU%|6cH~Z!i#K{EsKi}3b&;d#s6GXP$a?51bO`dRfyB@M_U{WuTfE_M_JW&5EF-ygWlM^`&p~|twSFYa8LuPDxn+()DB9j z!I=z;5}_GQ(hW+9Hjk?Lk}Z_$-oKDoMr6MwV!TA|!zR)~->8#kQ4~a~hN~ss>&wrG>r4Kr{F1 zJO0+}h*E+Ci%QIbfPtegNeYduDM1JXpw@r)p>mScGlK!9iWWF zsHEYk3R7XTMjnk(1$=wAOl$N+g8*POXlpXykic**(Kb}MHfNb)Wrik35xPMQj-?se zT(gt3X;nZf=tReDB;Glypc@1=F9^~K^aZ2o2?RI-Ve5Xw1aBXm0L^dx`t{3yHTM3% zP-_J8UVu3O*Wu4hMpqKGPVGS@^rDNPq3bMy*X&Q{mO12!l*Rhn?~-!X7O-*y1d=wC zkoC|EEmXwce#3Xu0G<=8$(hJiTyJ6u22X2>4Y;Z=c=2*V2H&wU0007mNklm`2Hm~R(Y){8kHYnmI%i~D;iq}KlmT)i>lqA=(cvV&TZ-MTYgvzpIHcPmw#7fZ) zFq}$-0mtU`DLRpus^Guzo#oR3$8P-M3nm64m*vs_~Yt*De2X^*0*1mbF_ z;aE&3*5Xve3J`-J_o59>X6Q8a+KVf4KUyYu)fJaDFhvDxC~EmOals<&4wpFvgdwQB zxC#Xg5)HYMB>jdK20Fk-N zn{Bpfw%B5e=H!!4Za)9~^XASw?`-<@>zDVpwzf9ke*10n{`>Fev#-DYdUL}KH#9fi zcw_#4zySv|zy0=G^ZobVH=Av?S#!-b*EH^V_uY4!jW^!7>EFM9bIdWvG)Ek9ME>U9 zhaY~px$U;wnnxaaq}hG<-J9Qk|Gimdl~o#hJ@d>n%{k|s)0}qNX&HmKzWeUGrY-@* zph1J8JQU@D`0~pyqXZlmTyQ~DRTV3)xMFna(j|KJ>J=Y;_+h;8!VA&4bLUuSp@pJs z8ne$ndyF4HKCZj&y1du*5@>w<@yBuX)mO){#~vH^+;dNi9z8lHPMjFeJ@;G;A3i)L zPoA8=4H+^dKKtymT(5ig?itH8(@YZu%-rjbKmLgK-g__Ro_p??ciwq3KKqGtzWL^h zJ@?!*F1_^9sQ>=^?>OX;Lt@ylVe#|NKgSwttPz`UzIn_!=bW+Wrkh4vTU(Us#8+Q^ z6(^iI?|D0=tq9cP|-W<2%O zQ!(wd)5ff`&KhT*eRfQmG%5dIaAU`gjURsaA(mcx=~#K?m2=Gn7FZyY1e}-(`mylB z3x^~nH^U4w#9x2?m7s|YfP)7Qj@l10T7LQEZ+^a%{SkS#~ynugYw&tKmHgCF1TPUx#W@wipoqk-E?v9z4ykG zPd*tZo_JzbrlqAN1Y2kp%PzZYCJxXl1-E~)t962(pC-_%ieKq$g)QUa! z*dx|mcis5<>#sAmLaPK|wbfRO+QwZ9b&fvz=tQjOAY)b86n(|kTW=k6%rQq4y#=s}#%DtPe02gge< zy%eXOdTO+k3eZUra7;b*)G^OI^JImK=#wJ%-+zD7_T`sfPWq`93ZZr}h`9dx>*JMI zUWp?95U1G0Am;fJCVm{qQ_sEL2IIyTr~gXAR6 z+PDKnBIQh~0VU4^1X`D0etF>kOf$`t&n~gV646!+LxTJ6yKic**lnVC#~pXXnrp6^ zfn&yl4?dW|F$L8IPPcB|Qk$xRn(lF(;JKG|X{nYG?y=NopM5qh7Lw2-s!J}pB())6 zC_|9t_9}KPh(G`QGbCNSJZ`z=mMp)dJ&%&}&p&?%N_EdW@4Vn-#4QMGbHbHYS}Dej z8yCwgvrM>OHM9iOoMo0-((XU{=%ciG&Pw>D2c`A9UV35FxX9nP-FDmHLiz^E9C+Y? zsRJB8^2j68A}wG;cIkHuKnDQ&g3Y(uYOAzq^sye>l@x9GTj2lv^H1swc=`hnR8WQQ zzWeUf1gEs7*h*7Pmm+lW(j%i`Jj_D!HIWMd=Uns<1W`vt4n6eHq?@LOeu7_Topq8f zS6+E#ikho%CAKXAG-H(#U3nCT5#-Ax8>z!{K%%qEj(mXcD(b>I|sxxH+D-W9nDM?hYlT@)Zpo8 zgSXR9KYdv5zWeS=w*r9fOmq57RaLR&mRrWHx854u=k(K0&u@;YvTRnMKrmIP0wB;0 z5SViN?Y9T5)$Q%=q4Wd^8Ji27$Z%Yu8lZ8+y{ymQI=GSwm}}XrYuB#v>#x72jS@by zAbe|{amE?RX-Eq~CG-NxxyHdz$uvQKbca^7rNUxV;VDz5Warmp85Jhcd5yRG@4tV} zJ!`MMcFs4@w}=hdqTV84PQeOo$Pmy79m~KOY?clL&pc35<%}!zqziiVa#M(7ITzKJy={$Y=_RYCQKT(z5y?Y0z5ftI_ zd83AaSvAK(6^ZV$%Pu*0DL;3k4cA$wx1K!L+QOH7&XfG6$+;PVp7qo--dISzDeHqIW z4BYPDzkkx`;)^fN!4gG*gf@7OZjvNk(}3{@>VlULaA{Kofry&!vyRHBG%!^b1%Xzx zS`)AY1m?S{svJ#}jdMvt@C2tq<-8FB$5RMeP`Oz%)Fd{zOr&UILwXaG@dI5Sdg!6- z#wG}+K?g<}ai|y?qmcVsTU*2Z)=_DLsp0jt*Io<8q8b3NIgd`OD#U6r!hzVd$Y#TUUzIG1_MS;Ww1WDz)>nX^#Ib5J%7Fa}0p;&l%I0K-JkR%Ovm z^?=MVT{B=x$XX_LOsVmc)v_^swZVGptrtpYbKO^BP}5Unue6fZr3K3*(9dr+^Uab~ zk1@~*Q&2^fVgP95BM56q>fCeB4b!~{ z*dw^^-Mi;`gKMd*%_L+d>ZqpVA*(7(Llg8tHO<&D8bdTpNpMZ{QDt%2lTNsvH8fuO z;fl4^Pt<;dI=yfR{MV@PmZKS6B%>mNp%uel$7q5QDu9~&U+FbXAUyEE18E-mgwCvi zu}}?IFwl1mMXfD(PUS4`nto-RMr}MoCoBn^XpX0|pGpQJ$vIO)OPaBYJP-9djM`XdVYsXg;Z0GWJMSoFAw1k=@ z-H$e&L1Q|hFO=gtg7y&yAY9{;#us3jG`Q7{JMNg1byZb?vY28m7F{cu=H~}u#&I20 zQ$!6nqKcOQkR(i2X~RK-28I5j{P?Q^KKRT&P0G--o_?hA+{uh3I8Nst{l?!IfLhD| zCGF`MdjTB!1h21&lMSmZGpKb{3*);H1j@LnQ3|4C_GbiWrSvLiGiWO>=aDdkX$i6i z9dwWwa*WWyZOl6TtvAq08tqmC-}Dwbzr@PFl~y40N@z9Fm#EY#xzzz14pGiV+}mE4hm_o_F$=+Ap<~3 zdWF6h5fVT}6l4T|v$l`F-ERa^VGdcdF_a=+rE_SJ7;V^yQxQCx*&MC>Kl^0UL@r1} zAv!@$q5xs50#aeg^escFUd31h>lKtrs5(tp69CqtC0b($<~MrKJJ_^y2+cS{8EH6W zT~$@S<=`@q5u_JqN`|*k(G&%1oS+Gt*#xaLFmxbLZ&x^*K7*>LMn~3ZDUFwb=q-$a zka3*tn-l=##A&U)_S!2c=~*u<00fZaCCbA*Fh}Uq|*q zF9{3ObKWZpNcFiQ!J?+dh<0d1@bt)yjP@F$YYm&u}f(Kr8(A@wc_jYp7x>7Mx8P007KquB(*90OvZ;Pz+VY z!M*5_R;&#W7VmE$nA>vtRg(k=EYUe zNOCCU9{q>#ISi3A6Es#a^BN%=k2t4*z28z9_Ro6JotrrgO4>*dC_uWGG1bNjqT~iL zCTfTn{Gg$q2pZ*Zn#j79w|`Ytg$Y+;oyx17wzjtL4M~%MgHlyh`EM)^LY_B8S5*}z z1^g#?d>6cDRYd|Fyq6?OvUA|+bIeFWUgG)p9H2Wn=Fo_HI4=N0nI1vxP*4rD!C_5` zTbZx865-;j;5mw>sYycDJ?1u3HBelWj0wUSLm-|*5!WF6_~Vb~TTOJN6O}cqd5dEW za~4~6m}BQa^nxbdP!Hb9*GOW&95J+((A``5? zB?2L|Q+8BSCB23=(1WAkt{{vTP-YYm1BT~ZXB2GLTSbXkL-RC6NKD`wFd7*uWT1{k&MAftl{*vH?lp#l!0 z8fU>rU0*N`STtp9kt89qWuDI6P{!`jw0gdAT`=UL-+SP}CD3MsR}E)zJm*Btgs)KCJwy0007eNklLnT1^1OTW3C18;6?4{+EGN=n1!QgRr%Hb3P6SRMlR<5cX7D)E2 z3ECp87COX912#6zvO(oE9tfH_D67*Rp_!On6C@W=Y5^*@HCVN^(a!yzWzkHes;cn1 zRkc;#;R>~kM%MH!w*nmMX$edKP-a3@H0sc4?Yy9W=-g;PT&xnpPGU0lpczM?1cwTm zsBh2F0LnlV!u*|eLlQNJo~CThAfS$lteOK6y5}8TQOkQUPG>scRQ943KwAq-$iVDo z-g5S-3Cp<|_lb+G%AU>Ys)DtF#i7FhRLV;V_K2(c3wl}9w|n&!2Qbqv-FZDKDf1I+ zmzWCZ1Dc>8z5kd2g3U;@Ww*Chr)n<~8u z3f48Knw-&7Rn0(jhk`1F`v8F~s}!7ODnP-y4g_ulBy8xQGEJbXs$qb2m8 sW&Q^M0RR6!5*j=J000I_L_t&o0Kc>0GtA!BPyhe`07*qoM6N<$f~bEX%>V!Z literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/random() colors a basic shader (WebGPU)/metadata.json b/test/unit/visual/screenshots/WebGPU/Shaders/random() colors a basic shader (WebGPU)/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Shaders/random() colors a basic shader (WebGPU)/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file From a473381d9feda1de7ac62f720e2053ab1f5ce906 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 27 Apr 2026 15:05:05 -0400 Subject: [PATCH 179/250] Save + restore 2D text canvas context when font is applied --- src/core/p5.Renderer3D.js | 10 ++++++++++ test/unit/webgl/p5.RendererGL.js | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 1fde0670bc..7a851dc4ad 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -1327,6 +1327,13 @@ export class Renderer3D extends Renderer { return this; } + push() { + super.push() + if (this.states.textFont?.font) { + this.textDrawingContext()?.save() + } + } + pop(...args) { if ( this._clipDepths.length > 0 && @@ -1334,6 +1341,9 @@ export class Renderer3D extends Renderer { ) { this._clearClip(); } + if (this.states.textFont?.font) { + this.textDrawingContext()?.restore() + } super.pop(...args); this._applyStencilTestIfClipping(); } diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 4d3eaaf7aa..5a082a4e81 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -2998,4 +2998,23 @@ suite('p5.RendererGL', function() { expect(myp5.get(5, 5)).toEqual([255, 0, 0, 255]); }); }); + + suite('fontWidth', function() { + test('respects textSize changes across push/pop', async function() { + myp5.createCanvas(100, 100, myp5.WEBGL); + const font = await myp5.loadFont('test/unit/assets/acmesa.ttf'); + + myp5.push(); + myp5.textFont(font); + myp5.textSize(12); + myp5.push(); + myp5.textSize(20); + const widthAt20 = myp5.fontWidth('X'); + myp5.pop(); + const widthAt12 = myp5.fontWidth('X'); + myp5.pop(); + + expect(widthAt20).toBeGreaterThan(widthAt12); + }); + }); }); From 8bd620b8e6bf2e7b65c3c43c784db138d31083fc Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 27 Apr 2026 15:50:00 -0400 Subject: [PATCH 180/250] Update to handle mid-push updates --- src/core/p5.Renderer3D.js | 11 +++++++---- test/unit/webgl/p5.RendererGL.js | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 7a851dc4ad..04ad1fbce9 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -160,6 +160,7 @@ export class Renderer3D extends Renderer { // clipping this._clipDepths = []; + this._textContextSavedStack = []; this._isClipApplied = false; this._stencilTestOn = false; @@ -1329,9 +1330,11 @@ export class Renderer3D extends Renderer { push() { super.push() - if (this.states.textFont?.font) { - this.textDrawingContext()?.save() + const saved = !!(this.states.textFont?.font); + if (saved) { + this.textDrawingContext().save() } + this._textContextSavedStack.push(saved); } pop(...args) { @@ -1341,8 +1344,8 @@ export class Renderer3D extends Renderer { ) { this._clearClip(); } - if (this.states.textFont?.font) { - this.textDrawingContext()?.restore() + if (this._textContextSavedStack.pop()) { + this.textDrawingContext().restore() } super.pop(...args); this._applyStencilTestIfClipping(); diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 5a082a4e81..26c406e22b 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -3016,5 +3016,20 @@ suite('p5.RendererGL', function() { expect(widthAt20).toBeGreaterThan(widthAt12); }); + + test('fontWidth restores correctly when font is unset inside push/pop', async function() { + myp5.createCanvas(100, 100, myp5.WEBGL); + const font = await myp5.loadFont('test/unit/assets/acmesa.ttf'); + myp5.textFont(font); + myp5.textSize(12); + myp5.push(); + myp5.textFont('sans-serif'); // unset loaded font + myp5.pop(); + // After pop, should be back to size 12 with loaded font + const widthAfterPop = myp5.fontWidth('X'); + myp5.textSize(20); + const widthAt20 = myp5.fontWidth('X'); + expect(widthAfterPop).toBeLessThan(widthAt20); + }); }); }); From 2f7206c9d85d80a9c3caf29d049f697b133f3ec8 Mon Sep 17 00:00:00 2001 From: nityam Date: Tue, 28 Apr 2026 22:06:07 +0530 Subject: [PATCH 181/250] support map() inside p5.strands shaders --- src/strands/strands_api.js | 17 +++++++++++++++ test/unit/webgl/p5.Shader.js | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index df7e8e8481..0bf7cfcf8f 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -294,6 +294,23 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } }); + const originalMap = fn.map; + augmentFn(fn, p5, 'map', function (...args) { + if (!strandsContext.active) { + return originalMap.apply(this, args); + } + const [n, start1, stop1, start2, stop2, withinBounds] = args; + const nNode = p5.strandsNode(n); + const start1Node = p5.strandsNode(start1); + const stop1Node = p5.strandsNode(stop1); + const t = nNode.sub(start1Node).div(stop1Node.sub(start1Node)); + const result = fn.mix(start2, stop2, t); + if (withinBounds) { + return fn.clamp(result, fn.min(start2, stop2), fn.max(start2, stop2)); + } + return result; + }); + augmentFn(fn, p5, 'getTexture', function (...rawArgs) { if (strandsContext.active) { const { id, dimension } = strandsContext.backend.createGetTextureCall(strandsContext, rawArgs); diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 1e307f51cc..527e946407 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -495,6 +495,46 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca }); + test('map() works inside a strands modify callback', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + const v = myp5.map(0.25, 0, 1, 0, 1); + inputs.color = [v, v, v, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 64, 5); + assert.approximately(pixelColor[1], 64, 5); + assert.approximately(pixelColor[2], 64, 5); + }); + + test('map() with withinBounds clamps the result inside strands', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + const v = myp5.map(2.0, 0, 1, 0, 1, true); + inputs.color = [v, v, v, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); + assert.approximately(pixelColor[1], 255, 5); + assert.approximately(pixelColor[2], 255, 5); + }); + test('handle custom uniform names with automatic values', () => { myp5.createCanvas(50, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => { From 708a4e59f6df0c338b3d4d1dde6e42a5e53b856d Mon Sep 17 00:00:00 2001 From: kushal Date: Tue, 28 Apr 2026 23:13:00 +0530 Subject: [PATCH 182/250] added unit test for loop protection error --- test/unit/webgl/p5.Shader.js | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 1e307f51cc..2b1293cc32 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -2373,6 +2373,20 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca }); suite('p5.strands error messages', () => { + const expectLoopProtectionError = run => { + let err; + + try { + run(); + } catch (e) { + err = e; + } + + assert.instanceOf(err, Error); + assert.include(err.message, 'Loop protection'); + assert.include(err.message, '// noprotect'); + }; + afterEach(() => { mockUserError.mockClear(); }); @@ -2447,5 +2461,40 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca assert.include(errMsg, 'float1'); assert.include(errMsg, 'float4'); }); + + test('shows a helpful error for web editor loop protection', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + expectLoopProtectionError(() => { + myp5.baseFilterShader().modify(() => { + myp5.filterColor.begin(); + { + loopProtect.protect({ line: 11, reset: true }); + for (let i = 0; i < 10; i++) { + if (loopProtect.protect({ line: 11 })) { + break; + } + } + } + myp5.filterColor.set([1, 0, 0, 1]); + myp5.filterColor.end(); + }, { myp5 }); + }); + }); + + test('shows a helpful error for OpenProcessing loop protection', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + expectLoopProtectionError(() => { + myp5.baseFilterShader().modify(() => { + myp5.filterColor.begin(); + for (let i = 0; i < 10; i++) { + window.$OP && $OP.loopProtect({ line: 11, ch: 2 }); + } + myp5.filterColor.set([1, 0, 0, 1]); + myp5.filterColor.end(); + }, { myp5 }); + }); + }); }); }); From a74163eeb30c603f7b66db11424543c32d14df81 Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Wed, 29 Apr 2026 17:32:24 +0530 Subject: [PATCH 183/250] Refactor: Extract shared addCopyingAndReturn helper for control flow transformations --- src/strands/strands_transpiler.js | 204 +++++++++++------------------- 1 file changed, 74 insertions(+), 130 deletions(-) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index 5a3211c4ba..716e035ad4 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -288,6 +288,79 @@ function transformBinaryOrLogical(node, state, ancestors) { node.arguments = [node.right]; } +// Shared helper used by both IfStatement and ForStatement handlers. +// Adds temp variable copies, replaces references, and appends a return +// statement to a branch/loop function body. +// sourcePrefix: the root identifier to read from ('vars' for loops, +// null for if-branches where we read directly from the outer variable). +function addCopyingAndReturn(functionBody, varsToReturn, sourcePrefix = null) { + if (functionBody.type !== 'BlockStatement') return; + + const tempVarMap = new Map(); + const copyStatements = []; + + for (const varPath of varsToReturn) { + const parts = varPath.split('.'); + const tempName = `__copy_${parts.join('_')}_${blockVarCounter++}`; + tempVarMap.set(varPath, tempName); + + // If sourcePrefix is set (loop case), read from vars.x.y + // Otherwise (if-branch case), read directly from x.y + let sourceExpr = sourcePrefix + ? { type: 'Identifier', name: sourcePrefix } + : { type: 'Identifier', name: parts[0] }; + + const pathParts = sourcePrefix ? parts : parts.slice(1); + for (const part of pathParts) { + sourceExpr = { + type: 'MemberExpression', + object: sourceExpr, + property: { type: 'Identifier', name: part }, + computed: false + }; + } + + copyStatements.push({ + type: 'VariableDeclaration', + declarations: [{ + type: 'VariableDeclarator', + id: { type: 'Identifier', name: tempName }, + init: { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: sourceExpr, + property: { type: 'Identifier', name: 'copy' }, + computed: false + }, + arguments: [] + } + }], + kind: 'let' + }); + } + + functionBody.body.forEach(node => replaceReferences(node, tempVarMap)); + functionBody.body.unshift(...copyStatements); + + const returnObj = { + type: 'ObjectExpression', + properties: Array.from(varsToReturn).map(varPath => ({ + type: 'Property', + key: { type: 'Literal', value: varPath }, + value: { type: 'Identifier', name: tempVarMap.get(varPath) }, + kind: 'init', + computed: false, + shorthand: false + })) + }; + + functionBody.body.push({ + type: 'ReturnStatement', + argument: returnObj + }); +} + const ASTCallbacks = { UnaryExpression(node, state, ancestors) { if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { @@ -689,70 +762,6 @@ const ASTCallbacks = { analyzeBranch(thenFunction.body); analyzeBranch(elseFunction.body); if (assignedVars.size > 0) { - // Add copying, reference replacement, and return statements to branch functions - const addCopyingAndReturn = (functionBody, varsToReturn) => { - if (functionBody.type === 'BlockStatement') { - // Create temporary variables and copy statements - const tempVarMap = new Map(); // property path -> temp name - const copyStatements = []; - for (const varPath of varsToReturn) { - const parts = varPath.split('.'); - const tempName = `__copy_${parts.join('_')}_${blockVarCounter++}`; - tempVarMap.set(varPath, tempName); - - // Build the member expression for the property path - let sourceExpr = { type: 'Identifier', name: parts[0] }; - for (let i = 1; i < parts.length; i++) { - sourceExpr = { - type: 'MemberExpression', - object: sourceExpr, - property: { type: 'Identifier', name: parts[i] }, - computed: false - }; - } - - // let tempName = propertyPath.copy() - copyStatements.push({ - type: 'VariableDeclaration', - declarations: [{ - type: 'VariableDeclarator', - id: { type: 'Identifier', name: tempName }, - init: { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: sourceExpr, - property: { type: 'Identifier', name: 'copy' }, - computed: false - }, - arguments: [] - } - }], - kind: 'let' - }); - } - // Apply reference replacement to all statements - functionBody.body.forEach(node => replaceReferences(node, tempVarMap)); - // Insert copy statements at the beginning - functionBody.body.unshift(...copyStatements); - // Add return statement with flat object using property paths as keys - const returnObj = { - type: 'ObjectExpression', - properties: Array.from(varsToReturn).map(varPath => ({ - type: 'Property', - key: { type: 'Literal', value: varPath }, - value: { type: 'Identifier', name: tempVarMap.get(varPath) }, - kind: 'init', - computed: false, - shorthand: false - })) - }; - functionBody.body.push({ - type: 'ReturnStatement', - argument: returnObj - }); - } - }; addCopyingAndReturn(thenFunction.body, assignedVars); addCopyingAndReturn(elseFunction.body, assignedVars); // Create a block variable to capture the return value @@ -1057,73 +1066,8 @@ const ASTCallbacks = { }); if (assignedVars.size > 0) { - // Add copying, reference replacement, and return statements similar to if statements - const addCopyingAndReturn = (functionBody, varsToReturn) => { - if (functionBody.type === 'BlockStatement') { - const tempVarMap = new Map(); - const copyStatements = []; - - for (const varPath of varsToReturn) { - const parts = varPath.split('.'); - const tempName = `__copy_${parts.join('_')}_${blockVarCounter++}`; - tempVarMap.set(varPath, tempName); - - // Build the member expression for vars.propertyPath - // e.g., vars.inputs.color or vars.x - let sourceExpr = { type: 'Identifier', name: 'vars' }; - for (const part of parts) { - sourceExpr = { - type: 'MemberExpression', - object: sourceExpr, - property: { type: 'Identifier', name: part }, - computed: false - }; - } - - copyStatements.push({ - type: 'VariableDeclaration', - declarations: [{ - type: 'VariableDeclarator', - id: { type: 'Identifier', name: tempName }, - init: { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: sourceExpr, - property: { type: 'Identifier', name: 'copy' }, - computed: false - }, - arguments: [] - } - }], - kind: 'let' - }); - } - - functionBody.body.forEach(node => replaceReferences(node, tempVarMap)); - functionBody.body.unshift(...copyStatements); - - // Add return statement with flat object using property paths as keys - const returnObj = { - type: 'ObjectExpression', - properties: Array.from(varsToReturn).map(varPath => ({ - type: 'Property', - key: { type: 'Literal', value: varPath }, - value: { type: 'Identifier', name: tempVarMap.get(varPath) }, - kind: 'init', - computed: false, - shorthand: false - })) - }; - - functionBody.body.push({ - type: 'ReturnStatement', - argument: returnObj - }); - } - }; - addCopyingAndReturn(bodyFunction.body, assignedVars); + addCopyingAndReturn(bodyFunction.body, assignedVars, 'vars'); // Create block variable and assignments similar to if statements const blockVar = `__block_${blockVarCounter++}`; From 113099c14e0c6149c786581cc0e4fd318c858b24 Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Wed, 29 Apr 2026 23:13:03 +0530 Subject: [PATCH 184/250] Docs: Add pipeline overview for transpileStrandsToJS --- src/strands/strands_transpiler.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index 5a3211c4ba..02fd8cf37d 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -1653,6 +1653,33 @@ function transformHelperFunctionEarlyReturns(ast, names) { } } +/** + * Transpiles a p5.strands callback into executable JavaScript by applying + * a multi-pass AST transformation pipeline. + * + * Pipeline stages: + * + * 1. Collect uniform callback names + * - Identifies functions passed into uniform() so they are excluded from transformation + * + * 2. transformSetCallsInControlFlow + * - Rewrites `.set()` calls inside control flow into intermediate variable assignments + * + * 3. Non-control-flow transformations + * - Applies ASTCallbacks to transform expressions, assignments, etc. + * - Skips IfStatement and ForStatement (handled later) + * + * 4. transformHelperFunctionEarlyReturns + * - Converts early returns in helper functions into a single return value pattern + * + * 5. Control flow transformation (post-order) + * - Transforms IfStatement → __p5.strandsIf + * - Transforms ForStatement → __p5.strandsFor + * - Handles variable propagation across branches/loops + * + * This staged approach ensures correct ordering and avoids transformation conflicts. + */ + export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { // Reset counters at the start of each transpilation blockVarCounter = 0; From dbf47090ffabaae9845e677eab2a4c770885852e Mon Sep 17 00:00:00 2001 From: harshiltewari2004 Date: Thu, 30 Apr 2026 20:19:07 +0530 Subject: [PATCH 185/250] =?UTF-8?q?Fix=20JSDoc=20typos:=20widhts=20?= =?UTF-8?q?=E2=86=92=20widths,=20coordniates=20=E2=86=92=20coordinates,=20?= =?UTF-8?q?coordiante=20=E2=86=92=20coordinate=20(dev-2.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/helpers.js | 4 ++-- src/webgl/utils.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/helpers.js b/src/core/helpers.js index 9f31e78d8a..277fbf2f5f 100644 --- a/src/core/helpers.js +++ b/src/core/helpers.js @@ -15,7 +15,7 @@ function modeAdjust(a, b, c, d, mode) { if (mode === constants.CORNER) { // CORNER mode already corresponds to a bounding box (top-left corner, width, height). - // For negative widhts or heights, the absolute value is used. + // For negative widths or heights, the absolute value is used. bbox = { x: a, y: b, @@ -26,7 +26,7 @@ function modeAdjust(a, b, c, d, mode) { } else if (mode === constants.CORNERS) { // CORNERS mode uses two opposite corners, in any configuration. - // Make sure to get the top left corner by using the minimum of the x and y coordniates. + // Make sure to get the top left corner by using the minimum of the x and y coordinates. bbox = { x: Math.min(a, c), y: Math.min(b, d), diff --git a/src/webgl/utils.js b/src/webgl/utils.js index 6944df60da..a5742c3f50 100644 --- a/src/webgl/utils.js +++ b/src/webgl/utils.js @@ -7,8 +7,8 @@ import { Texture } from "./p5.Texture"; * @param {Uint8Array|Float32Array|undefined} pixels An existing pixels array to reuse if the size is the same * @param {WebGLRenderingContext} gl The WebGL context * @param {WebGLFramebuffer|null} framebuffer The Framebuffer to read - * @param {Number} x The x coordiante to read, premultiplied by pixel density - * @param {Number} y The y coordiante to read, premultiplied by pixel density + * @param {Number} x The x coordinate to read, premultiplied by pixel density + * @param {Number} y The y coordinate to read, premultiplied by pixel density * @param {Number} width The width in pixels to be read (factoring in pixel density) * @param {Number} height The height in pixels to be read (factoring in pixel density) * @param {GLEnum} format Either RGB or RGBA depending on how many channels to read From 496ada9884e8a1de189ccfe6aa315c9213d58feb Mon Sep 17 00:00:00 2001 From: Perminder Singh Date: Sun, 3 May 2026 15:22:27 +0530 Subject: [PATCH 186/250] making callIndex runtime --- src/strands/p5.strands.js | 2 -- src/strands/strands_api.js | 27 +++++++----------- src/webgl/shaders/functions/randomGLSL.glsl | 6 +++- .../shaders/functions/randomVertGLSL.glsl | 6 +++- .../shaders/functions/randomComputeWGSL.js | 8 ++++-- .../shaders/functions/randomVertWGSL.js | 8 ++++-- src/webgpu/shaders/functions/randomWGSL.js | 8 ++++-- test/unit/visual/cases/webgpu.js | 22 ++++++++++++++ .../000.png | Bin 0 -> 4205 bytes .../metadata.json | 3 ++ 10 files changed, 64 insertions(+), 26 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/random() in a fragment loop averages to gray (WebGPU)/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/random() in a fragment loop averages to gray (WebGPU)/metadata.json diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 4667ddf42d..0089b37dd2 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -49,7 +49,6 @@ function strands(p5, fn) { ctx.windowOverrides = {}; ctx.fnOverrides = {}; ctx.graphicsOverrides = {}; - ctx._randomCallCount = 0; ctx._randomSeed = null; if (active) { p5.disableFriendlyErrors = true; @@ -66,7 +65,6 @@ function strands(p5, fn) { ctx.computeDeclarations = new Set(); ctx.hooks = []; ctx.active = false; - ctx._randomCallCount = 0; ctx._randomSeed = null; p5.disableFriendlyErrors = ctx.previousFES; for (const key in ctx.windowOverrides) { diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index d7d199aa97..400f94190c 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -385,7 +385,6 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }); strandsContext._randomSeed = null; - strandsContext._randomCallCount = 0; augmentFn(fn, p5, 'randomSeed', function (seed) { if (!strandsContext.active) { @@ -425,35 +424,31 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { ); } - const callIndex = strandsContext._randomCallCount++; - - const nodeArgs = [seedNode, callIndex]; + // The shader-side random() owns a private per-invocation counter, so a + // single AST node still produces distinct values across runtime loop + // iterations. We just pass the seed. + const nodeArgs = [seedNode]; + const randomOverloads = [{ + params: [DataType.float1], + returnType: DataType.float1, + }]; if (args.length === 0) { const { id, dimension } = build.functionCallNode(strandsContext, 'random', nodeArgs, { - overloads: [{ - params: [DataType.float1, DataType.float1], - returnType: DataType.float1, - }] + overloads: randomOverloads, }); return createStrandsNode(id, dimension, strandsContext); } else if (args.length === 1) { // random(max) → [0, max) const rawNode = build.functionCallNode(strandsContext, 'random', nodeArgs, { - overloads: [{ - params: [DataType.float1, DataType.float1], - returnType: DataType.float1, - }] + overloads: randomOverloads, }); const rawStrandsNode = createStrandsNode(rawNode.id, rawNode.dimension, strandsContext); return rawStrandsNode.mult(p5.strandsNode(args[0])); } else if (args.length === 2) { // random(min, max) → [min, max) const rawNode = build.functionCallNode(strandsContext, 'random', nodeArgs, { - overloads: [{ - params: [DataType.float1, DataType.float1], - returnType: DataType.float1, - }] + overloads: randomOverloads, }); const rawStrandsNode = createStrandsNode(rawNode.id, rawNode.dimension, strandsContext); const minNode = p5.strandsNode(args[0]); diff --git a/src/webgl/shaders/functions/randomGLSL.glsl b/src/webgl/shaders/functions/randomGLSL.glsl index 58bdc2fc96..0894462373 100644 --- a/src/webgl/shaders/functions/randomGLSL.glsl +++ b/src/webgl/shaders/functions/randomGLSL.glsl @@ -4,14 +4,18 @@ // α₂ = 1/φ₂² = 0.5698402910 // 1/φ = 0.6180339887 (golden ratio conjugate) +int _p5_randomCallIndex = 0; + float _p5_hash(vec3 p) { p = fract(p * vec3(0.1031, 0.1030, 0.0973)); p += dot(p, p.yxz + 33.33); return fract((p.x + p.y) * p.z); } -float random(float seed, float callIndex) { +float random(float seed) { vec2 pixelCoord = gl_FragCoord.xy; + float callIndex = float(_p5_randomCallIndex); + _p5_randomCallIndex += 1; // fract(seed * α₁) normalizes large seeds (e.g. performance.now()) into [0,1) // and spreads them optimally via the R₂ sequence's plastic constant float s = fract(seed * 0.7548776662); diff --git a/src/webgl/shaders/functions/randomVertGLSL.glsl b/src/webgl/shaders/functions/randomVertGLSL.glsl index 2940be1b96..2c9b1128bb 100644 --- a/src/webgl/shaders/functions/randomVertGLSL.glsl +++ b/src/webgl/shaders/functions/randomVertGLSL.glsl @@ -4,14 +4,18 @@ // α₂ = 1/φ₂² = 0.5698402910 // 1/φ = 0.6180339887 (golden ratio conjugate) +int _p5_randomCallIndex = 0; + float _p5_hash(vec3 p) { p = fract(p * vec3(0.1031, 0.1030, 0.0973)); p += dot(p, p.yxz + 33.33); return fract((p.x + p.y) * p.z); } -float random(float seed, float callIndex) { +float random(float seed) { float vid = float(gl_VertexID); + float callIndex = float(_p5_randomCallIndex); + _p5_randomCallIndex += 1; float s = fract(seed * 0.7548776662); return _p5_hash(vec3( vid + s, diff --git a/src/webgpu/shaders/functions/randomComputeWGSL.js b/src/webgpu/shaders/functions/randomComputeWGSL.js index 9b9b17379c..321ed4640a 100644 --- a/src/webgpu/shaders/functions/randomComputeWGSL.js +++ b/src/webgpu/shaders/functions/randomComputeWGSL.js @@ -4,17 +4,21 @@ // α₂ = 1/φ₂² = 0.5698402910 // 1/φ = 0.6180339887 (golden ratio conjugate) // -// Compute shader version: invocationId is passed in from main via @builtin(global_invocation_id) +// Compute shader version: invocationId is passed in from main via @builtin(global_invocation_id). export default ` +var _p5_randomCallIndex: i32 = 0; + fn _p5_hash(p: vec3) -> f32 { var p3 = fract(p * vec3(0.1031, 0.1030, 0.0973)); p3 = p3 + dot(p3, p3.yxz + 33.33); return fract((p3.x + p3.y) * p3.z); } -fn random(seed: f32, callIndex: f32, invocationId: vec3) -> f32 { +fn random(seed: f32, invocationId: vec3) -> f32 { let id = vec3(invocationId); + let callIndex = f32(_p5_randomCallIndex); + _p5_randomCallIndex = _p5_randomCallIndex + 1; let s = fract(seed * 0.7548776662); return _p5_hash(vec3( id.x + s, diff --git a/src/webgpu/shaders/functions/randomVertWGSL.js b/src/webgpu/shaders/functions/randomVertWGSL.js index 008a5448cd..210d6c49c8 100644 --- a/src/webgpu/shaders/functions/randomVertWGSL.js +++ b/src/webgpu/shaders/functions/randomVertWGSL.js @@ -4,16 +4,20 @@ // α₂ = 1/φ₂² = 0.5698402910 // 1/φ = 0.6180339887 (golden ratio conjugate) // -// Vertex shader version: vertexId is passed in from main via @builtin(vertex_index) +// Vertex shader version: vertexId is passed in from main via @builtin(vertex_index). export default ` +var _p5_randomCallIndex: i32 = 0; + fn _p5_hash(p: vec3) -> f32 { var p3 = fract(p * vec3(0.1031, 0.1030, 0.0973)); p3 = p3 + dot(p3, p3.yxz + 33.33); return fract((p3.x + p3.y) * p3.z); } -fn random(seed: f32, callIndex: f32, vertexId: f32) -> f32 { +fn random(seed: f32, vertexId: f32) -> f32 { + let callIndex = f32(_p5_randomCallIndex); + _p5_randomCallIndex = _p5_randomCallIndex + 1; let s = fract(seed * 0.7548776662); return _p5_hash(vec3( vertexId + s, diff --git a/src/webgpu/shaders/functions/randomWGSL.js b/src/webgpu/shaders/functions/randomWGSL.js index 3e0b69d289..62005bd2a4 100644 --- a/src/webgpu/shaders/functions/randomWGSL.js +++ b/src/webgpu/shaders/functions/randomWGSL.js @@ -4,16 +4,20 @@ // α₂ = 1/φ₂² = 0.5698402910 // 1/φ = 0.6180339887 (golden ratio conjugate) // -// Fragment shader version: pixelCoord is passed in from main via @builtin(position) +// Fragment shader version: pixelCoord is passed in from main via @builtin(position). export default ` +var _p5_randomCallIndex: i32 = 0; + fn _p5_hash(p: vec3) -> f32 { var p3 = fract(p * vec3(0.1031, 0.1030, 0.0973)); p3 = p3 + dot(p3, p3.yxz + 33.33); return fract((p3.x + p3.y) * p3.z); } -fn random(seed: f32, callIndex: f32, pixelCoord: vec2) -> f32 { +fn random(seed: f32, pixelCoord: vec2) -> f32 { + let callIndex = f32(_p5_randomCallIndex); + _p5_randomCallIndex = _p5_randomCallIndex + 1; let s = fract(seed * 0.7548776662); return _p5_hash(vec3( pixelCoord.x + s, diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 50d696d434..b7c613603d 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -342,6 +342,28 @@ visualSuite("WebGPU", function () { p5.plane(50, 50); await screenshot(); }); + + visualTest('random() in a fragment loop averages to gray (WebGPU)', async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + const shader = p5.baseMaterialShader().modify(() => { + p5.randomSeed(7); + p5.getPixelInputs(inputs => { + let sum = p5.float(0.0); + for (let i = 0; i < 20; i++) { + sum = sum + p5.random(); + } + const avg = sum / 20; + inputs.color = [avg, avg, avg, 1.0]; + return inputs; + }); + }, { p5 }); + + p5.background(0); + p5.noStroke(); + p5.shader(shader); + p5.plane(50, 50); + await screenshot(); + }); }); diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/random() in a fragment loop averages to gray (WebGPU)/000.png b/test/unit/visual/screenshots/WebGPU/Shaders/random() in a fragment loop averages to gray (WebGPU)/000.png new file mode 100644 index 0000000000000000000000000000000000000000..5e19d4c98852855f40ad124b2b2190faf2e31963 GIT binary patch literal 4205 zcmV-z5R&hSP)$;!o=lbFLy|_!T1~do>e}Ntn=s*Y%EP)1)0D%sKCbUUt@B@IL!QI{6 zUDw-R@%GCjSuB}zR*f1ps^(hzRHH_XswYjFRF51vGS9B3PMum$oH((bHEUMAXwjm2 z{P^+p{Q2{9e8PkY^@0Tp>akVjsx^!thXU?1) zH-GZv$^WytV#SJj+_-V|^y$;<*|TTY3l}cT^W3>}bB(zxSFWt5Oqr5%7B600j~+d` zUb19Ky=>XCdfK#UiN{*c*r~q%{`>Ow+i#aY{`jN3e*L<9`st_T{rBH5@4fe4`QU>O z%C&3PN?Ti7dH(!)`R=>#%8M5-%AGrRO84&F^K7oQ#{c^3uUv1=*|TTM*~anU!Gm)6 z@Zs|6)vI##>ecjd;J|@$@7}%g&A^6<;s;SrCYad<+IN|OMLv^zJ0q?5m7$>{PW`9pMU;Ye*E#rbjI-~pL~)* z?cKXKgMIn(WqJ4Ack?)U^k`uKXkZ0>`t<2?=+L3^=+UF{(MKO;P`7T~Do>w2Enj{0 zRT(^ZaDwgBsZ$b%EcPy3xKMuj>8AwOr%#^*yJN?W!iPWwiNx#IuY*~3?b=lY_3+`t zyzkkwXQ>+h5}1b$V6gr1#~&wnAba%ak>Et%($bQF3-sBuX9>_8th4&}-+yO-#$*eV z2ysRP@W+lF{{8#q=bwKrY{4mO#@G~eb?)4`lt@IB zufP7feEH>Pb&}IoQ2>cj3?)0(SyMmeo;-Q7$ZkYr{m2UYCr+FwYE4$HsVoB8BFqTJ8EX-d`UVzgl2b#1APMXW znhk+sUm%)>^?+l}S|lh`a~1Dzzx|dOGtZcH03$=~$&mFF!dFBTbIfP|_uqdnWQ;`X zD$Xxmx|BvQU%s5h&5#a+C1BNt>&=@tXMIQ~d2s?9V|(`O$xajz*`Zn+nPyu?XrC-1 zqNs(4C3rxhkxzdVd zS+R~$EU61?jRPj#KxhI$?QL{EfXGr15kd9^tNif;xa)|>cFaHht1SV`gqqsFe}Br2 zJreBDp+l(vV3;8Q9dt+nzyM6k*zeuDch-}D7{IuIWr!p4YS*q^b{4Ii4eL24g>S$8 zHanrL2#&9&9r6PR4g^sI_Q{EF_7O!bGRGIvn!YPRL`1ejg=ardU<~Nn+MRRX4TbCWzY@4Uftd&W2H4o3NQ9ERAC8H7*?U`&s`R_m3U<7ahM7&vKm=Q*ifK|pZgH5(bfZSxmn8M~v(Cyo|&z6o1 zS;2B^qn;eHf)Mlh@zwz-Qe__j)FE)bNe&^#D@+5eAQ=%11DFFAQ|4fab08xk_Z!X- zuyg0m+%R3!F9OKO&ddhg^C2_pKjY31ks~noTC4G9Lt)oAq6Fd^mW~}e=7#J( zyB1dI5NM0EJE;W%sDJO`2f?q`k`?k)r~g>EX$gV zA&~^J-PYEYf?&O(q^8swWA(ILTVxC=q1tVy+LN>?*7Gdm>m`;dbc-bZ0d}#y< zgSwMJjB$t=-~@#|fyj=z3;}=@#VfPU_8<}>6+i$`G&tfb%uqOb|q!=?;mTJKo)3pT1 z0XdL{`ZKRNu8#Nzz zQ-KBmw>hv@QjIO?VT?0O0w5#6D@^{`GiG~$O9ZPY+1{0Zip6$ zRv?1VO{7^tVVv%PXAB_D=(5=W)RVy6s~0wa0@BCk zLd7<=+yjIV*M0l;E$kqJl=u~>8YICHBSvI)bvqLC48hoD2e|?vB}5>>dQvmLk}i>9 z)IxP4LKDc2(}>8=RRNFyV_v1bl3-Y>NstX0lNr|$kvl#ac?Oigoymm{g^M5(0!XHG zNWlY-aq{6uwh#j^l93<@h|6M^M&G}pWEk5f10YLS1WZrY1c3GWA#3!^I1=buM%c67 zI>2=`65~`2$_@e*I<~zjIP-4}!UV?-ut*Rz!uT=IGvq}?{)AQea>IfEECf^_Udwed zfJg^e!Ri!(!x|^_E5+CZ1~{^@25{M9n_StFQNPZR(7JW&GSk2}AV@(h;Cu0u3h-jTnyPN>RxggwQ z6ftT>L0gYJ*#yut^ZBt>Cd`3_KhlqgVt+!a;49#yhy)l0f-@4(>MYcWFlJ1E*w#e^ zJbd`@{G~;JT5U^9OMWAmhfR`@72s%|FE;J${E#bO}x)h!Z#uvf)o_HngQDa z6>xqbiXP*1N6YR_5*)(%y%F6fO6su0XWah5hPF=iGn3F0U$~i zyd_&BKtN?j0h6vZG9kl)!ajR2AO}YG6-jZ14U?tZYk^iLC^aA`P8oMrJLZ#;wVwG} zV^rXr3trYRDIo7ju4Q8V`t@0$K*=A(Oum-5YSEw?j+b|vx`TG^UdJdqh35pE_uxUN8){_B}fYsW~mj!EN+k9uO z7+JuR7;=#)Q^-KL=dw$lYA=6j#4deHD_|B{-~dY685fXy&alBawgIx=&_6POmTv3m zT^AD=f*kC~7N0VYd4~)Ma=?HAMV15@5y`$Wdk35Rnji7z>y_UM5XMU5pD0D>-ek@_ z0~`y|>KXtCrp*Bs^8*JC%t9wGSyD5cSOWlEyZ3hB)uc%d9|)8YSy;V#b^eI~7PW<# zCLZ!al9JfAZCn27fwsg!9aY5SC?%OzaQ(zZwys z=S{K%W(~$MN)kGb=em$j0DG0SRDm3sL6CLuAP*_<+DM&|P1= zsdddzBYF9U9E=K{GerjwX9|@})dZVB`G7OudO;vh5JQI!Eyy5A`<6n71z0K|L0Lqg z>>G!fFZhs^*HQ^6<}eQwXV_3X*tS4=rIR7`!LarSxYvC81*~7ceyM3=>`FU(>?27~ zfXb46%N#-5++eZJnY>{@kZfq#8UT{aWs_tz0KruL!@flZG0!+gWXeXxa6}|P@&N=3 z#uYB60rR6C;Ibfa*+K-VI-AeA;s?MQ#96~9U&hG=L7O&h${#n3TcfUw!zn1rK{kJt zq7=OyzH8U636>GR9ts9XZO$ByoymYS1Qih}Gi%sFiVVt_?2(se>;TZ#_~}V!;!k!2 zKt8~*XTAfW=CLCq=DXLv*_VC62__;+RT}?H#D{tY*cTK`NJU5`-HMHI zj!1lx`0HxV{Mmh4XhJB0yL_*e+ z8Zy)Yn`#8Jz^Wy_Jwu29*)oPq66Bv<8z*cetMylYx3!QXlK|w*VFHE(%BZu2zWphRueWl`%o_g@nq_mD2BffV$e z<4U7(uJUi%9Gd_LgSt#a=Xc4!U|o&%sg z%NCXd$PvI~Ckq(mjC}z+$NnD00000NkvXXu0mjf DcBZq? literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/random() in a fragment loop averages to gray (WebGPU)/metadata.json b/test/unit/visual/screenshots/WebGPU/Shaders/random() in a fragment loop averages to gray (WebGPU)/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Shaders/random() in a fragment loop averages to gray (WebGPU)/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file From e06784bab7aa7543ad3e038ae46b5724070e3bd3 Mon Sep 17 00:00:00 2001 From: Perminder Singh <127239756+perminder-17@users.noreply.github.com> Date: Sun, 3 May 2026 22:59:37 +0530 Subject: [PATCH 187/250] Refactor setHeading to check vector dimensions --- src/math/p5.Vector.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index e73d15540e..e9e2ac9df7 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -2043,17 +2043,13 @@ class Vector { * } */ setHeading(a) { - if (this.isPInst) a = this._toRadians(a); - if (this.y === undefined || (this.z !== undefined && this.z !== 0)) { - const p5inst = this.isPInst ? this._pInst : (typeof p5 !== 'undefined' ? p5 : null); - if (p5inst && p5inst._friendlyError) { - p5inst._friendlyError( - 'p5.Vector.setHeading() only supports 2D vectors (z === 0). ' + - 'For 3D or higher-dimensional vectors, use rotate() or another ' + - 'appropriate method instead.', - 'p5.Vector.setHeading' - ); - } + if (this.dimensions < 2 || this._values.slice(2).some(v => v !== 0)) { + p5._friendlyError( + 'p5.Vector.setHeading() only supports 2D vectors (z === 0). ' + + 'For 3D or higher-dimensional vectors, use rotate() or another ' + + 'appropriate method instead.', + 'p5.Vector.setHeading' + ); return this; } const m = this.mag(); From e21a46b94e6e7e0bbf8bc6cc7ada5a74362b08cc Mon Sep 17 00:00:00 2001 From: nityam Date: Mon, 4 May 2026 15:42:26 +0530 Subject: [PATCH 188/250] address review: switch fn. to this. and add range/gradient tests --- src/strands/strands_api.js | 4 +- test/unit/webgl/p5.Shader.js | 82 ++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 0bf7cfcf8f..3c64aa48ab 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -304,9 +304,9 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const start1Node = p5.strandsNode(start1); const stop1Node = p5.strandsNode(stop1); const t = nNode.sub(start1Node).div(stop1Node.sub(start1Node)); - const result = fn.mix(start2, stop2, t); + const result = this.mix(start2, stop2, t); if (withinBounds) { - return fn.clamp(result, fn.min(start2, stop2), fn.max(start2, stop2)); + return this.clamp(result, this.min(start2, stop2), this.max(start2, stop2)); } return result; }); diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 527e946407..dbf45f1f56 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -535,6 +535,88 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca assert.approximately(pixelColor[2], 255, 5); }); + test('map() shrinks a wider input range into a narrower output range', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + const v = myp5.map(5, 0, 10, 0, 1); + inputs.color = [v, v, v, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 128, 5); + assert.approximately(pixelColor[1], 128, 5); + assert.approximately(pixelColor[2], 128, 5); + }); + + test('map() handles offset output ranges correctly', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + const v = myp5.map(0.5, 0, 1, 0.2, 0.8); + inputs.color = [v, v, v, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 128, 5); + assert.approximately(pixelColor[1], 128, 5); + assert.approximately(pixelColor[2], 128, 5); + }); + + test('map() handles a negative input range', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + const v = myp5.map(0, -1, 1, 0, 1); + inputs.color = [v, v, v, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 128, 5); + assert.approximately(pixelColor[1], 128, 5); + assert.approximately(pixelColor[2], 128, 5); + }); + + test('map() remaps texCoord.x into a horizontal gradient', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + const v = myp5.map(inputs.texCoord.x, 0, 1, 0.2, 0.8); + inputs.color = [v, v, v, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + const left = myp5.get(2, 25); + const middle = myp5.get(25, 25); + const right = myp5.get(47, 25); + assert.approximately(left[0], 51, 10); + assert.approximately(middle[0], 128, 10); + assert.approximately(right[0], 204, 10); + }); + test('handle custom uniform names with automatic values', () => { myp5.createCanvas(50, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => { From 547c52a57ab1640271b474c0dded561b972c9203 Mon Sep 17 00:00:00 2001 From: SOUMITRO-SAHA Date: Mon, 4 May 2026 18:09:59 +0530 Subject: [PATCH 189/250] - Allowing get() method - Enable array indexing for storage and vector buffers - Added validation for array literals in shaders (2-4 elements only) --- src/strands/strands_node.js | 11 ++-- src/strands/strands_transpiler.js | 11 +++- src/webgl/strands_glslBackend.js | 6 ++ test/unit/webgpu/p5.Shader.js | 93 +++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 9 deletions(-) diff --git a/src/strands/strands_node.js b/src/strands/strands_node.js index 1528a0fc3c..f7638855bc 100644 --- a/src/strands/strands_node.js +++ b/src/strands/strands_node.js @@ -166,18 +166,15 @@ export class StrandsNode { } get(index) { - // Validate baseType is 'storage' const nodeData = getNodeDataFromID(this.strandsContext.dag, this.id); - if (nodeData.baseType !== 'storage') { - throw new Error('get() can only be used on storage buffers'); - } - // For struct storage, return a proxy with per-field getters/setters - if (this._schema) { + // Validate baseType is 'storage' + // For struct storage buffers, return a proxy with per-field getters/setters + if (nodeData.baseType === 'storage' && this._schema) { return createStructArrayElementProxy(this.strandsContext, this, index, this._schema); } - // Create array access node: buffer.get(index) -> buffer[index] + // Create array access node for storage and non-storage (vector) access const { id, dimension } = arrayAccessNode( this.strandsContext, this, diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index bf33907338..8cc0f1e76a 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -569,6 +569,13 @@ const ASTCallbacks = { if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { return; } + + if (node.elements.length < 2 || node.elements.length > 4) { + throw new Error( + `Array literals in shader functions are transpiled to vectors and must have 2-4 elements (got ${node.elements.length}).` + ); + } + const original = JSON.parse(JSON.stringify(node)); node.type = 'CallExpression'; node.callee = { @@ -1240,8 +1247,8 @@ const ASTCallbacks = { delete node.update; }, - - + + } // Helper function to check if a function body contains return statements in control flow diff --git a/src/webgl/strands_glslBackend.js b/src/webgl/strands_glslBackend.js index 9ec04b4fae..0876547a89 100644 --- a/src/webgl/strands_glslBackend.js +++ b/src/webgl/strands_glslBackend.js @@ -330,6 +330,12 @@ export const glslBackend = { const parentExpr = this.generateExpression(generationContext, dag, parentID); return `${parentExpr}.${node.swizzle}`; } + if (node.opCode === OpCode.Binary.ARRAY_ACCESS) { + const [bufferID, indexID] = node.dependsOn; + const bufferExpr = this.generateExpression(generationContext, dag, bufferID); + const indexExpr = this.generateExpression(generationContext, dag, indexID); + return `${bufferExpr}[${indexExpr}]`; + } if (node.dependsOn.length === 2) { const [lID, rID] = node.dependsOn; const left = this.generateExpression(generationContext, dag, lID); diff --git a/test/unit/webgpu/p5.Shader.js b/test/unit/webgpu/p5.Shader.js index 6571d92edb..eb9bb79990 100644 --- a/test/unit/webgpu/p5.Shader.js +++ b/test/unit/webgpu/p5.Shader.js @@ -1269,5 +1269,98 @@ suite('WebGPU p5.Shader', function() { }).not.toThrow(); }); }); + + suite('array indexing on non-storage vectors (#8756)', () => { + test('indexing into array returned from helper function does not throw', async () => { + await myp5.createCanvas(5, 5, myp5.WEBGPU); + const storage = myp5.createStorage(new Float32Array(4)); + + const computeShader = myp5.buildComputeShader(() => { + const data = myp5.uniformStorage(); + function getArray() { + return [1, 2]; + } + const arr = getArray(); + data[myp5.index.x] = arr[0] + arr[1] + myp5.index.x; + }, { myp5 }); + + computeShader.setUniform('data', storage); + + expect(() => { + myp5.compute(computeShader, 4); + }).not.toThrow(); + }); + + test('inline literal indexing [1, 2][0] works end-to-end', async () => { + await myp5.createCanvas(5, 5, myp5.WEBGPU); + const storage = myp5.createStorage(new Float32Array(4)); + + const computeShader = myp5.buildComputeShader(() => { + const data = myp5.uniformStorage(); + data[myp5.index.x] = [1, 2][0]; + }, { myp5 }); + + computeShader.setUniform('data', storage); + + expect(() => { + myp5.compute(computeShader, 4); + }).not.toThrow(); + }); + + test('array literal with 1 element throws descriptive error', async () => { + await myp5.createCanvas(5, 5, myp5.WEBGPU); + + expect(() => { + myp5.buildComputeShader(() => { + const data = myp5.uniformStorage(); + const arr = [1]; + data[myp5.index.x] = arr[0]; + }, { myp5 }); + }).toThrow('and must have 2-4 elements (got 1)'); + }); + + test('array literal with 5 elements throws descriptive error', async () => { + await myp5.createCanvas(5, 5, myp5.WEBGPU); + + expect(() => { + myp5.buildComputeShader(() => { + const data = myp5.uniformStorage(); + const arr = [1, 2, 3, 4, 5]; + data[myp5.index.x] = arr[0]; + }, { myp5 }); + }).toThrow('and must have 2-4 elements (got 5)'); + }); + + test('valid array lengths 2, 3, 4 do not throw', async () => { + await myp5.createCanvas(5, 5, myp5.WEBGPU); + const storage = myp5.createStorage(new Float32Array(4)); + + expect(() => { + const s2 = myp5.buildComputeShader(() => { + const data = myp5.uniformStorage(); + const arr = [1, 2]; + data[myp5.index.x] = arr[0]; + }, { myp5 }); + s2.setUniform('data', storage); + myp5.compute(s2, 4); + + const s3 = myp5.buildComputeShader(() => { + const data = myp5.uniformStorage(); + const arr = [1, 2, 3]; + data[myp5.index.x] = arr[0]; + }, { myp5 }); + s3.setUniform('data', storage); + myp5.compute(s3, 4); + + const s4 = myp5.buildComputeShader(() => { + const data = myp5.uniformStorage(); + const arr = [1, 2, 3, 4]; + data[myp5.index.x] = arr[0]; + }, { myp5 }); + s4.setUniform('data', storage); + myp5.compute(s4, 4); + }).not.toThrow(); + }); + }); }); }); From 7867f4b76829386f539dce145a6beffae8437491 Mon Sep 17 00:00:00 2001 From: kit Date: Thu, 7 May 2026 13:16:32 +0200 Subject: [PATCH 190/250] Fix failing test on setHeading --- src/math/p5.Vector.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index f8eea78b0e..40b0a7ccaa 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -1981,7 +1981,9 @@ class Vector { * } */ setHeading(a) { - if (this.dimensions < 2 || this._values.slice(2).some(v => v !== 0)) { + if (this.dimensions < 2 || ( + this._values instanceof Array && this._values.slice(2).some(v => v !== 0)) + ) { p5._friendlyError( 'p5.Vector.setHeading() only supports 2D vectors (z === 0). ' + 'For 3D or higher-dimensional vectors, use rotate() or another ' + From e0e70d05f2024c857cefc1fe65e3c5f5e7e44727 Mon Sep 17 00:00:00 2001 From: harshiltewari2004 Date: Sun, 10 May 2026 19:16:17 +0530 Subject: [PATCH 191/250] Rename States.getDiff to takeDiff to reflect mutation semantics --- src/core/States.js | 2 +- src/core/p5.Renderer.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/States.js b/src/core/States.js index a70a6300c2..bc5a8a9a41 100644 --- a/src/core/States.js +++ b/src/core/States.js @@ -14,7 +14,7 @@ export class States { this[key] = value; } - getDiff() { + takeDiff() { const diff = this.#modified; this.#modified = {}; return diff; diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 8467695bf8..ae5082644a 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -135,7 +135,7 @@ class Renderer { // and push it into the push pop stack push() { this._pushPopDepth++; - this._pushPopStack.push(this.states.getDiff()); + this._pushPopStack.push(this.states.takeDiff()); } // Pop the previous states out of the push pop stack and From b54e54073301ad8fda4555275f7092aa2815de0f Mon Sep 17 00:00:00 2001 From: harshiltewari2004 Date: Sun, 10 May 2026 19:17:25 +0530 Subject: [PATCH 192/250] Add unit tests for States class --- test/unit/core/States.js | 70 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 test/unit/core/States.js diff --git a/test/unit/core/States.js b/test/unit/core/States.js new file mode 100644 index 0000000000..d4b2b56a35 --- /dev/null +++ b/test/unit/core/States.js @@ -0,0 +1,70 @@ +import { States } from '../../../src/core/States.js'; + +suite('States', function () { + test('initialises with provided state', function () { + const s = new States({ fill: 'red', stroke: 'blue' }); + assert.equal(s.fill, 'red'); + assert.equal(s.stroke, 'blue'); + }); + + test('setValue() updates the value', function () { + const s = new States({ fill: 'red' }); + s.setValue('fill', 'green'); + assert.equal(s.fill, 'green'); + }); + + test('setValue() records the original before first modification', function () { + const s = new States({ fill: 'red' }); + s.setValue('fill', 'green'); + assert.equal(s.getModified().fill, 'red'); + }); + + test('setValue() does not overwrite original on repeated calls', function () { + const s = new States({ fill: 'red' }); + s.setValue('fill', 'green'); + s.setValue('fill', 'blue'); + assert.equal(s.getModified().fill, 'red'); + }); + + test('takeDiff() returns the modified map', function () { + const s = new States({ fill: 'red' }); + s.setValue('fill', 'green'); + const diff = s.takeDiff(); + assert.equal(diff.fill, 'red'); + }); + + test('takeDiff() clears #modified after returning', function () { + const s = new States({ fill: 'red' }); + s.setValue('fill', 'green'); + s.takeDiff(); + assert.deepEqual(s.takeDiff(), {}); + }); + + test('applyDiff() undoes current modifications and replaces #modified', function () { + const s = new States({ fill: 'red' }); + s.setValue('fill', 'green'); + const outerModified = {}; + s.applyDiff(outerModified); + assert.equal(s.fill, 'red'); + assert.deepEqual(s.getModified(), {}); + }); + + test('applyDiff() with a non-empty prevModified replaces #modified', function () { + const s = new States({ fill: 'red', stroke: 'black' }); + s.setValue('fill', 'green'); + const outerModified = { stroke: 'black' }; + s.applyDiff(outerModified); + assert.equal(s.fill, 'red'); + assert.deepEqual(s.getModified(), { stroke: 'black' }); + }); + + test('push/pop cycle: applyDiff restores prior state', function () { + const s = new States({ fill: 'red' }); + const beforePush = s.takeDiff(); + s.setValue('fill', 'green'); + assert.equal(s.fill, 'green'); + s.applyDiff(beforePush); + assert.equal(s.fill, 'red'); + assert.deepEqual(s.getModified(), {}); + }); +}); \ No newline at end of file From 4396d3b039dcc12c476d7b7f1ed9c06ac26ab67d Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Mon, 11 May 2026 11:35:05 +0530 Subject: [PATCH 193/250] Refactor: Extract runNonControlFlowPass, runControlFlowPass, and buildStrandsCallback from transpileStrandsToJS --- src/strands/strands_transpiler.js | 97 +++++++++++++++++-------------- 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index bf33907338..265ccf4755 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -1682,34 +1682,30 @@ function transformHelperFunctionEarlyReturns(ast, names) { * This staged approach ensures correct ordering and avoids transformation conflicts. */ -export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { - // Reset counters at the start of each transpilation - blockVarCounter = 0; - loopVarCounter = 0; - - const ast = parse(sourceString, { - ecmaVersion: 2021, - locations: srcLocations - }); - - throwIfLoopProtectionInserted(ast); - - // Pre-pass: collect names of functions passed by reference as uniform callbacks - const uniformCallbackNames = collectUniformCallbackNames(ast); - - // First pass: transform .set() calls in control flow to use intermediate variables - transformSetCallsInControlFlow(ast, uniformCallbackNames); +// Wraps each callback with a uniform context guard, eliminating the need +// to repeat the early-return check at the top of every handler. +function makeGuardedCallbacks(callbacks) { + const guarded = {}; + for (const [name, fn] of Object.entries(callbacks)) { + guarded[name] = (node, state, ancestors) => { + if (ancestors.some(a => + nodeIsUniform(a) || + nodeIsUniformCallbackFn(a, state.uniformCallbackNames) + )) return; + return fn(node, state, ancestors); + }; + } + return guarded; +} - // Second pass: transform everything except if/for statements using normal ancestor traversal - const nonControlFlowCallbacks = { ...ASTCallbacks }; +function runNonControlFlowPass(ast, uniformCallbackNames) { + const nonControlFlowCallbacks = makeGuardedCallbacks({ ...ASTCallbacks }); delete nonControlFlowCallbacks.IfStatement; delete nonControlFlowCallbacks.ForStatement; ancestor(ast, nonControlFlowCallbacks, undefined, { varyings: {}, uniformCallbackNames }); +} - // Third pass: transform helper functions with early returns to use __returnValue pattern - transformHelperFunctionEarlyReturns(ast, uniformCallbackNames); - - // Fourth pass: transform if/for statements in post-order using recursive traversal +function runControlFlowPass(ast, uniformCallbackNames) { const postOrderControlFlowTransform = { CallExpression(node, state, c) { if (nodeIsUniform(node)) { return; } @@ -1724,48 +1720,41 @@ export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { if ( state.uniformCallbackNames?.has(node.id?.name) && (node.init?.type === 'FunctionExpression' || node.init?.type === 'ArrowFunctionExpression') - ) { - return; - } + ) { return; } if (node.init) c(node.init, state); }, IfStatement(node, state, c) { state.inControlFlow++; - // First recursively process children if (node.test) c(node.test, state); if (node.consequent) c(node.consequent, state); if (node.alternate) c(node.alternate, state); - // Then apply the transformation to this node ASTCallbacks.IfStatement(node, state, []); state.inControlFlow--; }, ForStatement(node, state, c) { state.inControlFlow++; - // First recursively process children if (node.init) c(node.init, state); if (node.test) c(node.test, state); if (node.update) c(node.update, state); if (node.body) c(node.body, state); - // Then apply the transformation to this node ASTCallbacks.ForStatement(node, state, []); state.inControlFlow--; }, ReturnStatement(node, state, c) { if (!state.inControlFlow) return; - // Convert return statement to strandsEarlyReturn call node.type = 'ExpressionStatement'; node.expression = { type: 'CallExpression', - callee: { - type: 'Identifier', - name: '__p5.strandsEarlyReturn' - }, + callee: { type: 'Identifier', name: '__p5.strandsEarlyReturn' }, arguments: node.argument ? [node.argument] : [] }; delete node.argument; } }; recursive(ast, { varyings: {}, inControlFlow: 0, uniformCallbackNames }, postOrderControlFlowTransform); +} + +function buildStrandsCallback(p5, ast, scope) { const transpiledSource = escodegen.generate(ast); const scopeKeys = Object.keys(scope); const match = /\(?\s*(?:function)?\s*\w*\s*\(([^)]*)\)\s*(?:=>)?\s*{((?:.|\n)*)}\s*;?\s*\)?/ @@ -1785,15 +1774,7 @@ export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { } const body = match[2]; try { - const internalStrandsCallback = new Function( - // Create a parameter called __p5, not just p5, because users of instance mode - // may pass in a variable called p5 as a scope variable. If we rely on a variable called - // p5, then the scope variable called p5 might accidentally override internal function - // calls to p5 static methods. - '__p5', - ...paramNames, - body, - ); + const internalStrandsCallback = new Function('__p5', ...paramNames, body); return () => internalStrandsCallback(p5, ...paramVals); } catch (e) { console.error(e); @@ -1802,3 +1783,31 @@ export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { throw new Error('Error transpiling p5.strands callback!'); } } + + + +export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { + blockVarCounter = 0; + loopVarCounter = 0; + + const ast = parse(sourceString, { ecmaVersion: 2021, locations: srcLocations }); + + throwIfLoopProtectionInserted(ast); + + // Pre-pass: collect names of functions passed by reference as uniform callbacks + const uniformCallbackNames = collectUniformCallbackNames(ast); + + // Pass 1: transform .set() calls in control flow to use intermediate variables + transformSetCallsInControlFlow(ast, uniformCallbackNames); + + // Pass 2: transform non-control-flow nodes (operators, varyings, uniforms, arrays) + runNonControlFlowPass(ast, uniformCallbackNames); + + // Pass 3: transform helper functions with early returns to use __returnValue pattern + transformHelperFunctionEarlyReturns(ast, uniformCallbackNames); + + // Pass 4: transform if/for statements post-order into strandsIf/strandsFor calls + runControlFlowPass(ast, uniformCallbackNames); + + return buildStrandsCallback(p5, ast, scope); +} From e6d4b336f25b12a0a36219d80d477c7d2cae2d4d Mon Sep 17 00:00:00 2001 From: SOUMITRO-SAHA Date: Mon, 11 May 2026 13:55:56 +0530 Subject: [PATCH 194/250] - Replace generic Error with FES.userError for better error handling. - Add comprehensive tests for WebGL. --- src/strands/strands_transpiler.js | 3 +- test/unit/webgl/p5.Shader.js | 101 ++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index 8cc0f1e76a..e0c165dcae 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -571,7 +571,8 @@ const ASTCallbacks = { } if (node.elements.length < 2 || node.elements.length > 4) { - throw new Error( + FES.userError( + 'type error', `Array literals in shader functions are transpiled to vectors and must have 2-4 elements (got ${node.elements.length}).` ); } diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 2b1293cc32..0495c07184 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -542,6 +542,107 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca assert.approximately(pixelColor[2], 153, 5); }); + suite('array indexing on non-storage vectors (#8756)', () => { + afterEach(() => { + mockUserError.mockClear(); + }); + + test('indexing into array returned from helper function works in WebGL', () => { + myp5.createCanvas(5, 5, myp5.WEBGL); + const myShader = myp5.baseMaterialShader().modify(() => { + const brightness = myp5.uniformFloat(); + function getArr() { + return [1, 2]; + } + const arr = getArr(); + myp5.getPixelInputs(inputs => { + inputs.color = [arr[0] * brightness, arr[1], 0, 1]; + return inputs; + }); + }, { myp5 }); + expect(() => { + myp5.shader(myShader); + myp5.plane(myp5.width, myp5.height); + }).not.toThrowError(); + }); + + test('inline literal indexing [1, 2][0] works in WebGL', () => { + myp5.createCanvas(5, 5, myp5.WEBGL); + const myShader = myp5.baseMaterialShader().modify(() => { + const brightness = myp5.uniformFloat(); + myp5.getPixelInputs(inputs => { + inputs.color = [[1, 2][0] * brightness, 0, 0, 1]; + return inputs; + }); + }, { myp5 }); + expect(() => { + myp5.shader(myShader); + myp5.plane(myp5.width, myp5.height); + }).not.toThrowError(); + }); + + test('array literal with 1 element throws descriptive error in WebGL', () => { + myp5.createCanvas(5, 5, myp5.WEBGL); + expect(() => { + myp5.baseMaterialShader().modify(() => { + const arr = [1]; + myp5.getPixelInputs(inputs => { + inputs.color = [arr[0], 0, 0, 1]; + return inputs; + }); + }, { myp5 }); + }).toThrowError('and must have 2-4 elements (got 1)'); + }); + + test('array literal with 5 elements throws descriptive error in WebGL', () => { + myp5.createCanvas(5, 5, myp5.WEBGL); + expect(() => { + myp5.baseMaterialShader().modify(() => { + const arr = [1, 2, 3, 4, 5]; + myp5.getPixelInputs(inputs => { + inputs.color = [arr[0], 0, 0, 1]; + return inputs; + }); + }, { myp5 }); + }).toThrowError('and must have 2-4 elements (got 5)'); + }); + + test('valid array lengths 2, 3, 4 work in WebGL', () => { + myp5.createCanvas(5, 5, myp5.WEBGL); + expect(() => { + const s2 = myp5.baseMaterialShader().modify(() => { + const arr = [1, 2]; + myp5.getPixelInputs(inputs => { + inputs.color = [arr[0], 0, 0, 1]; + return inputs; + }); + }, { myp5 }); + myp5.shader(s2); + myp5.plane(myp5.width, myp5.height); + + const s3 = myp5.baseMaterialShader().modify(() => { + const arr = [1, 2, 3]; + myp5.getPixelInputs(inputs => { + inputs.color = [arr[0], 0, 0, 1]; + return inputs; + }); + }, { myp5 }); + myp5.shader(s3); + myp5.plane(myp5.width, myp5.height); + + const s4 = myp5.baseMaterialShader().modify(() => { + const arr = [1, 2, 3, 4]; + myp5.getPixelInputs(inputs => { + inputs.color = [arr[0], 0, 0, 1]; + return inputs; + }); + }, { myp5 }); + myp5.shader(s4); + myp5.plane(myp5.width, myp5.height); + }).not.toThrowError(); + }); + }); + suite('if statement conditionals', () => { test('handle simple if statement with true condition', () => { myp5.createCanvas(50, 50, myp5.WEBGL); From 143c1a1221b8c8331d93e09151a2a7900903ebdd Mon Sep 17 00:00:00 2001 From: kit Date: Mon, 11 May 2026 12:39:43 +0200 Subject: [PATCH 195/250] Vector documentaiton updates --- src/math/p5.Vector.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 40b0a7ccaa..cf288f0c42 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -10,14 +10,6 @@ import * as constants from '../core/constants'; */ const prioritizeSmallerDimension = function(currentVectorDimension, args) { return Math.min(currentVectorDimension, args.length); - - //if (args.length !== currentVectorDimension && args.length !== 1) { - // TODO how to suppress for valid solo arguments? - // this._friendlyError( - // `Operating on two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as ${minDimension}D vectors, and any additional values of the longer vector will be ignored.`, 'p5.Vector' - //); - //} - //return minDimension; }; @@ -36,7 +28,7 @@ class Vector { * * You can add (`add()`), multiply (`mult()`), divide (`div()`), and subtract (`sub()`) * vectors from each other, and calculate remainder (`rem()`). Only use these functions - * on vectors when they are the same size: for example, both 2D, or both 3D. + * on vectors when they are the same size: both 2-dimensional, or both 3-dimensional. * When an operation uses two vectors of different sizes, the smaller dimension will be * used, any additional values of the longer vector will be ignored. * @@ -87,6 +79,12 @@ class Vector { // This will get overwritten when exported as part of p5. _friendlyError(_e) {} + + /** + * Gets how many dimensions the vector has. + * + * @returns {Number} The number of dimensions. Can be 1, 2, or 3. + */ get dimensions(){ return this.values.length; } @@ -382,9 +380,10 @@ class Vector { * another p5.Vector object, as in `v.add(v2)`, or * an array of numbers, as in `v.add([1, 2, 3])`. * - * Add vectors only when they are the same size: both 2D, or both 3D. - * When two vectors of different sizes are added, the smaller dimension will be - * used, any additional values of the longer vector will be ignored. + * Add vectors only when they are the same size: both 2-dimensional, or + * both 3-dimensional. When two vectors of different sizes are added, the + * smaller dimension will be used, any additional values of the longer + * vector will be ignored. * For example, adding `[1, 2, 3]` and `[4, 5]` will result in `[5, 7]`. * * Calling `add()` with no arguments, as in `v.add()`, has no effect. @@ -1984,7 +1983,7 @@ class Vector { if (this.dimensions < 2 || ( this._values instanceof Array && this._values.slice(2).some(v => v !== 0)) ) { - p5._friendlyError( + p5._friendlyError( 'p5.Vector.setHeading() only supports 2D vectors (z === 0). ' + 'For 3D or higher-dimensional vectors, use rotate() or another ' + 'appropriate method instead.', From 9f775f4c7c4d013f6cacf196231befb8cb5e25fd Mon Sep 17 00:00:00 2001 From: kit Date: Mon, 11 May 2026 13:14:20 +0200 Subject: [PATCH 196/250] Mark .array() as deprecated --- src/math/p5.Vector.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index cf288f0c42..f5f1960ab1 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -2690,14 +2690,15 @@ class Vector { * Returns the vector's components as an array of numbers. * * @return {Number[]} array with the vector's components. + * @deprecated To retrieve vector components, use `v.values` * @example * // META:norender * function setup() { * // Create a p5.Vector object. * let v = createVector(20, 30); * - * // Prints "[20, 30, 0]" to the console. - * print(v.array()); + * // Prints "[20, 30]" to the console. + * print(v.values); * } */ array() { From 3d173f90e2916e414b1a0242b670f96899e08d7f Mon Sep 17 00:00:00 2001 From: kit Date: Mon, 11 May 2026 13:28:40 +0200 Subject: [PATCH 197/250] Clarify cross product docus --- src/math/p5.Vector.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index f5f1960ab1..7f2b23fd3f 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -1372,6 +1372,9 @@ class Vector { * The cross product is a vector that points straight out of the plane created * by two vectors. The cross product's magnitude is the area of the parallelogram * formed by the original two vectors. + * + * The cross product is defined on 3-dimensional vectors, and will use the `x`, `y`, + * and `z` components. This method should only be used with 3D vectors. * * The static version of `cross()`, as in `p5.Vector.cross(v1, v2)`, is the same * as calling `v1.cross(v2)`. From 57a5a306cbc593d73ce25d45225aaa297c55b8f3 Mon Sep 17 00:00:00 2001 From: kit Date: Mon, 11 May 2026 13:34:38 +0200 Subject: [PATCH 198/250] 2.3.0-rc.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ed36a0643..ed8c1c8305 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "p5", - "version": "2.2.3", + "version": "2.3.0-rc.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "p5", - "version": "2.2.3", + "version": "2.3.0-rc.0", "license": "LGPL-2.1", "dependencies": { "@davepagurek/bezier-path": "^0.0.7", diff --git a/package.json b/package.json index 63ff7b7711..3e15986808 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test/**/*.js": "eslint", "utils/**/*.{js,mjs}": "eslint" }, - "version": "2.2.3", + "version": "2.3.0-rc.0", "dependencies": { "@davepagurek/bezier-path": "^0.0.7", "@japont/unicode-range": "^1.0.0", From 0278bbd377dd6d0ac73c4aeb9a120cbf223a2d59 Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Sat, 9 May 2026 16:20:53 +0530 Subject: [PATCH 199/250] Add StorageBuffer.set(index, value) for single-element GPU updates --- src/webgpu/p5.RendererWebGPU.js | 108 ++++++++++++++++++++++++++ test/unit/webgpu/p5.RendererWebGPU.js | 67 ++++++++++++++++ 2 files changed, 175 insertions(+) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 2b4e6c7d82..0c2c592de1 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -246,6 +246,114 @@ function rendererWebGPU(p5, fn) { } return rawCopy; } + + /** + * Updates a single element in the buffer at the given index without + * rewriting the entire buffer. This is more efficient than `update()` + * when only one element needs to change. + * + * Uses WebGPU's `GPUQueue.writeBuffer()` with a byte offset, so the + * cost is proportional to one element rather than the whole buffer. + * + * ```js example + * let buf; + * + * async function setup() { + * await createCanvas(100, 100, WEBGPU); + * + * // Float buffer: update one value by index + * buf = createStorage(new Float32Array([1, 2, 3, 4])); + * buf.set(2, 9.5); // only index 2 changes → [1, 2, 9.5, 4] + * + * let result = await buf.read(); + * print(result[2]); // 9.5 + * describe('Prints 9.5 to the console.'); + * } + * ``` + * + * ```js example + * let particles; + * const numParticles = 100; + * + * async function setup() { + * await createCanvas(100, 100, WEBGPU); + * particles = createStorage(makeParticles()); + * + * // Struct buffer: replace particle 42 without touching others + * particles.set(42, { + * position: createVector(0, 0), + * velocity: createVector(1, 0), + * }); + * describe('Updates a single particle in a storage buffer.'); + * } + * + * function makeParticles() { + * let data = []; + * for (let i = 0; i < numParticles; i++) { + * data.push({ + * position: createVector(random(width), random(height)), + * velocity: createVector(random(-1, 1), random(-1, 1)), + * }); + * } + * return data; + * } + * ``` + * + * @method set + * @for p5.StorageBuffer + * @beta + * @webgpu + * @webgpuOnly + * @param {Number} index The zero-based index of the element to update. + * @param {Number|Object} value The new value. Pass a number for float + * buffers, or a plain object matching the original struct layout for + * struct buffers. + */ + set(index, value) { + const device = this._renderer.device; + + if (this._schema !== null) { + // buffer was created with an array of structs + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new Error( + 'set() expects a plain object matching the original struct format for this buffer' + ); + } + + const { stride } = this._schema; + const byteOffset = index * stride; + + if (byteOffset + stride > this.size) { + throw new Error( + `set() index ${index} is out of bounds for this buffer ` + + `(buffer holds ${Math.floor(this.size / stride)} elements)` + ); + } + + // pack just this one element using the same logic as update() + const packed = this._renderer._packStructArray([value], this._schema); + // use packed.buffer (ArrayBuffer) so the size arg is always in bytes + device.queue.writeBuffer(this.buffer, byteOffset, packed.buffer, 0, stride); + } else { + // buffer was created with a float array + if (typeof value !== 'number') { + throw new Error( + 'set() expects a number for this float buffer' + ); + } + + const byteOffset = index * 4; + + if (byteOffset + 4 > this.size) { + throw new Error( + `set() index ${index} is out of bounds for this buffer ` + + `(buffer holds ${Math.floor(this.size / 4)} floats)` + ); + } + + device.queue.writeBuffer(this.buffer, byteOffset, new Float32Array([value])); + } + } } /** diff --git a/test/unit/webgpu/p5.RendererWebGPU.js b/test/unit/webgpu/p5.RendererWebGPU.js index 936ab9a853..f21503be60 100644 --- a/test/unit/webgpu/p5.RendererWebGPU.js +++ b/test/unit/webgpu/p5.RendererWebGPU.js @@ -254,4 +254,71 @@ suite('WebGPU p5.RendererWebGPU', function() { } }); }); + + suite('StorageBuffer.set()', function() { + test('updates a single float value at the given index', async function() { + const buf = myp5.createStorage(new Float32Array([1, 2, 3, 4])); + buf.set(2, 9.5); + + const result = await buf.read(); + + expect(result[0]).to.be.closeTo(1, 0.001); + expect(result[1]).to.be.closeTo(2, 0.001); + expect(result[2]).to.be.closeTo(9.5, 0.001); // only this changed + expect(result[3]).to.be.closeTo(4, 0.001); + }); + + test('updates a single struct element without touching neighbours', async function() { + const input = [ + { x: 1.0, y: 2.0 }, + { x: 3.0, y: 4.0 }, + { x: 5.0, y: 6.0 }, + ]; + const buf = myp5.createStorage(input); + + // Replace only the middle element + buf.set(1, { x: 99.0, y: 88.0 }); + + const result = await buf.read(); + + expect(result.length).to.be.at.least(3); + // Element 0 unchanged + expect(result[0].x).to.be.closeTo(1.0, 0.001); + expect(result[0].y).to.be.closeTo(2.0, 0.001); + // Element 1 updated + expect(result[1].x).to.be.closeTo(99.0, 0.001); + expect(result[1].y).to.be.closeTo(88.0, 0.001); + // Element 2 unchanged + expect(result[2].x).to.be.closeTo(5.0, 0.001); + expect(result[2].y).to.be.closeTo(6.0, 0.001); + }); + + test('set() then read() reflects the new value immediately', async function() { + const buf = myp5.createStorage(new Float32Array([0, 0, 0])); + buf.set(0, 42); + + const result = await buf.read(); + expect(result[0]).to.be.closeTo(42, 0.001); + }); + + test('throws on out-of-bounds index for float buffer', function() { + const buf = myp5.createStorage(new Float32Array([1, 2, 3])); + expect(() => buf.set(10, 5.0)).to.throw(); + }); + + test('throws on out-of-bounds index for struct buffer', function() { + const buf = myp5.createStorage([{ x: 1.0 }, { x: 2.0 }]); + expect(() => buf.set(99, { x: 3.0 })).to.throw(); + }); + + test('throws when passing a non-number to a float buffer', function() { + const buf = myp5.createStorage(new Float32Array([1, 2, 3])); + expect(() => buf.set(0, { x: 1 })).to.throw(); + }); + + test('throws when passing a non-object to a struct buffer', function() { + const buf = myp5.createStorage([{ x: 1.0 }, { x: 2.0 }]); + expect(() => buf.set(0, 42)).to.throw(); + }); + }); }); From b52c39f7650f5abc77ac14e6d847de74badb6ee2 Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Sun, 10 May 2026 23:40:41 +0530 Subject: [PATCH 200/250] Update set() JSDoc per review feedback --- src/webgpu/p5.RendererWebGPU.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 0c2c592de1..0307104e5e 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -248,14 +248,12 @@ function rendererWebGPU(p5, fn) { } /** - * Updates a single element in the buffer at the given index without - * rewriting the entire buffer. This is more efficient than `update()` - * when only one element needs to change. + * Updates a single element in the buffer at a given index. Use this + * when only a small number of elements need to change. If you need to + * replace all the data at once, use + * `update()` instead. * - * Uses WebGPU's `GPUQueue.writeBuffer()` with a byte offset, so the - * cost is proportional to one element rather than the whole buffer. - * - * ```js example + * ```js * let buf; * * async function setup() { From d0e996f30e7ebbe635241f3ed389edc27f95a17a Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Mon, 11 May 2026 18:38:36 +0530 Subject: [PATCH 201/250] Add read() + print() to second set() example for visible output --- src/webgpu/p5.RendererWebGPU.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 0307104e5e..53af4c7100 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -253,7 +253,7 @@ function rendererWebGPU(p5, fn) { * replace all the data at once, use * `update()` instead. * - * ```js + * ```js example * let buf; * * async function setup() { @@ -277,12 +277,16 @@ function rendererWebGPU(p5, fn) { * await createCanvas(100, 100, WEBGPU); * particles = createStorage(makeParticles()); * - * // Struct buffer: replace particle 42 without touching others + * // Replace particle 42 without touching the others * particles.set(42, { * position: createVector(0, 0), * velocity: createVector(1, 0), * }); - * describe('Updates a single particle in a storage buffer.'); + * + * // Read back to confirm the update + * let result = await particles.read(); + * print(result[42].position.x, result[42].position.y); // 0, 0 + * describe('Prints the position of particle 42 after updating it.'); * } * * function makeParticles() { From b5594b32723143410f4d0657367cc31da427c7dd Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Mon, 11 May 2026 18:46:27 +0530 Subject: [PATCH 202/250] remove js example tag --- src/webgpu/p5.RendererWebGPU.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 53af4c7100..3b909230b7 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -253,7 +253,7 @@ function rendererWebGPU(p5, fn) { * replace all the data at once, use * `update()` instead. * - * ```js example + * ```js * let buf; * * async function setup() { @@ -269,7 +269,7 @@ function rendererWebGPU(p5, fn) { * } * ``` * - * ```js example + * ```js * let particles; * const numParticles = 100; * From 93e304ee3cbb35731b087bd51c11f03d8448dbf2 Mon Sep 17 00:00:00 2001 From: kit Date: Mon, 11 May 2026 15:54:28 +0200 Subject: [PATCH 203/250] update to array() docsrtring --- src/math/p5.Vector.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 7f2b23fd3f..0bc152162b 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -2700,8 +2700,8 @@ class Vector { * // Create a p5.Vector object. * let v = createVector(20, 30); * - * // Prints "[20, 30]" to the console. - * print(v.values); + * // Prints "[20, 30, 0]" to the console. + * print(v.array()); * } */ array() { From c626de443e75f41646f9b69a3e9cc621ef18bab0 Mon Sep 17 00:00:00 2001 From: kit Date: Mon, 11 May 2026 16:28:32 +0200 Subject: [PATCH 204/250] 2.3.0-rc.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed8c1c8305..4737ef01e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "p5", - "version": "2.3.0-rc.0", + "version": "2.3.0-rc.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "p5", - "version": "2.3.0-rc.0", + "version": "2.3.0-rc.1", "license": "LGPL-2.1", "dependencies": { "@davepagurek/bezier-path": "^0.0.7", diff --git a/package.json b/package.json index 3e15986808..ea9ace6a92 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test/**/*.js": "eslint", "utils/**/*.{js,mjs}": "eslint" }, - "version": "2.3.0-rc.0", + "version": "2.3.0-rc.1", "dependencies": { "@davepagurek/bezier-path": "^0.0.7", "@japont/unicode-range": "^1.0.0", From 4d785c9564acdc68d40e302ad251d97c3cb9b540 Mon Sep 17 00:00:00 2001 From: Lalit Narayan Yadav <162928571+LalitNarayanYadav@users.noreply.github.com> Date: Tue, 12 May 2026 12:52:09 +0530 Subject: [PATCH 205/250] Fix: Restore callback comment and remove guarded wrapper usage Updated nonControlFlowCallbacks to use spread operator correctly and added comments for clarity in internalStrandsCallback function. --- src/strands/strands_transpiler.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index 265ccf4755..1c4410d5d8 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -1699,7 +1699,7 @@ function makeGuardedCallbacks(callbacks) { } function runNonControlFlowPass(ast, uniformCallbackNames) { - const nonControlFlowCallbacks = makeGuardedCallbacks({ ...ASTCallbacks }); + const nonControlFlowCallbacks = ({ ...ASTCallbacks }); delete nonControlFlowCallbacks.IfStatement; delete nonControlFlowCallbacks.ForStatement; ancestor(ast, nonControlFlowCallbacks, undefined, { varyings: {}, uniformCallbackNames }); @@ -1775,6 +1775,10 @@ function buildStrandsCallback(p5, ast, scope) { const body = match[2]; try { const internalStrandsCallback = new Function('__p5', ...paramNames, body); + // Create a parameter called __p5, not just p5, because users of instance mode + // may pass in a variable called p5 as a scope variable. If we rely on a variable called + // p5, then the scope variable called p5 might accidentally override internal function + // calls to p5 static methods. return () => internalStrandsCallback(p5, ...paramVals); } catch (e) { console.error(e); From f1c2596d44c1713213b8e615d5fca63f32586f79 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 12 May 2026 09:30:06 -0400 Subject: [PATCH 206/250] Make sure all p5.strands math operators are converted to strands nodes --- src/strands/strands_transpiler.js | 23 ++++++++++------------- test/unit/webgl/p5.Shader.js | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index bf33907338..59fe718467 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -323,17 +323,14 @@ function transformBinaryOrLogical(node, state, ancestors) { if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { return; } - const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier']; - if (unsafeTypes.includes(node.left.type)) { - node.left = { - type: 'CallExpression', - callee: { - type: 'Identifier', - name: '__p5.strandsNode', - }, - arguments: [node.left] - }; - } + node.left = { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: '__p5.strandsNode', + }, + arguments: [node.left] + }; node.type = 'CallExpression'; node.callee = { type: 'MemberExpression', @@ -1240,8 +1237,8 @@ const ASTCallbacks = { delete node.update; }, - - + + } // Helper function to check if a function body contains return statements in control flow diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 7f42d589da..fd719c73ac 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -459,6 +459,21 @@ suite('p5.Shader', function() { }).not.toThrowError(); }); + test('buildFilterShader can use numeric constants from scope', () => { + myp5.createCanvas(5, 5, myp5.WEBGL); + const constants = { val: 100 }; + const myShader = myp5.buildFilterShader(({ constants }) => { + filterColor.begin(); + let c = 0; + c += constants.val / 255; + filterColor.set([c, c, c, 1]); + filterColor.end(); + }, { constants }); + expect(() => { + myp5.filter(myShader); + }).not.toThrowError(); + }); + test('buildMaterialShader forwards scope to modify', () => { myp5.createCanvas(5, 5, myp5.WEBGL); expect(() => { From 0dfed2b71bd305d79f4b36795be49c42dc1bc625 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 12 May 2026 09:58:46 -0400 Subject: [PATCH 207/250] Make line shader hooks use unmultiplied alpha --- src/webgl/shaders/line.frag | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/webgl/shaders/line.frag b/src/webgl/shaders/line.frag index b1ed298b0d..ec39236912 100644 --- a/src/webgl/shaders/line.frag +++ b/src/webgl/shaders/line.frag @@ -69,6 +69,7 @@ void main() { discard; } } - OUT_COLOR = HOOK_getFinalColor(vec4(inputs.color.rgb, 1.) * inputs.color.a, vec2(0.0, 0.0)); + OUT_COLOR = HOOK_getFinalColor(inputs.color, vec2(0.0, 0.0)); + OUT_COLOR.rgb *= OUT_COLOR.a; HOOK_afterFragment(); } From 6207afdc37681f81d1fd8c9148eb019b262b0074 Mon Sep 17 00:00:00 2001 From: kit Date: Tue, 12 May 2026 19:47:56 +0200 Subject: [PATCH 208/250] 2.3.0-rc.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4737ef01e1..a438439fdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "p5", - "version": "2.3.0-rc.1", + "version": "2.3.0-rc.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "p5", - "version": "2.3.0-rc.1", + "version": "2.3.0-rc.2", "license": "LGPL-2.1", "dependencies": { "@davepagurek/bezier-path": "^0.0.7", diff --git a/package.json b/package.json index ea9ace6a92..dd5b126379 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test/**/*.js": "eslint", "utils/**/*.{js,mjs}": "eslint" }, - "version": "2.3.0-rc.1", + "version": "2.3.0-rc.2", "dependencies": { "@davepagurek/bezier-path": "^0.0.7", "@japont/unicode-range": "^1.0.0", From 8876bbf28ed55fda1cdb3203ec5f50c3a3010de6 Mon Sep 17 00:00:00 2001 From: kit Date: Tue, 12 May 2026 22:09:17 +0200 Subject: [PATCH 209/250] 2.3.0-rc.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a438439fdd..6b8b1d8aff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "p5", - "version": "2.3.0-rc.2", + "version": "2.3.0-rc.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "p5", - "version": "2.3.0-rc.2", + "version": "2.3.0-rc.3", "license": "LGPL-2.1", "dependencies": { "@davepagurek/bezier-path": "^0.0.7", diff --git a/package.json b/package.json index dd5b126379..24045f6592 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test/**/*.js": "eslint", "utils/**/*.{js,mjs}": "eslint" }, - "version": "2.3.0-rc.2", + "version": "2.3.0-rc.3", "dependencies": { "@davepagurek/bezier-path": "^0.0.7", "@japont/unicode-range": "^1.0.0", From c23d1c0febae41bb0f3da99c47aa259f97dc17ff Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 13 May 2026 09:37:31 -0400 Subject: [PATCH 210/250] Don't use push(...arr) in GeometryBuilder for long vertex arrays --- src/webgl/GeometryBuilder.js | 24 +++++++++++++++--------- test/unit/webgl/p5.RendererGL.js | 17 +++++++++++++++++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index b59548bf67..777feb838e 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -64,11 +64,15 @@ class GeometryBuilder { } let startIdx = this.geometry.vertices.length; - this.geometry.vertices.push(...this.transformVertices(input.vertices)); - this.geometry.vertexNormals.push( - ...this.transformNormals(input.vertexNormals) - ); - this.geometry.uvs.push(...input.uvs); + for (const v of this.transformVertices(input.vertices)) { + this.geometry.vertices.push(v); + } + for (const vn of this.transformNormals(input.vertexNormals)) { + this.geometry.vertexNormals.push(vn); + } + for (const val of input.uvs) { + this.geometry.uvs.push(val); + } const inputUserVertexProps = input.userVertexProperties; const builtUserVertexProps = this.geometry.userVertexProperties; @@ -103,15 +107,17 @@ class GeometryBuilder { ); } if (this.renderer.states.strokeColor) { - this.geometry.edges.push( - ...input.edges.map(edge => edge.map(idx => idx + startIdx)) - ); + for (const edge of input.edges.map(edge => edge.map(idx => idx + startIdx))) { + this.geometry.edges.push(edge); + } } const vertexColors = [...input.vertexColors]; while (vertexColors.length < input.vertices.length * 4) { vertexColors.push(...this.renderer.states.curFillColor); } - this.geometry.vertexColors.push(...vertexColors); + for (const c of vertexColors) { + this.geometry.vertexColors.push(c); + } } /** diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 1b6b59c78f..aaa229bcec 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -3089,6 +3089,23 @@ suite('p5.RendererGL', function() { myp5.model(geom); expect(myp5.get(5, 5)).toEqual([255, 0, 0, 255]); }); + test('does not throw with a large number of vertices', function() { + myp5.createCanvas(10, 10, myp5.WEBGL); + // Enough triangles to exceed the ~65k argument limit of Function.prototype.apply, + // which would cause a stack overflow if vertices were spread into push() calls. + const numTriangles = 30000; + expect(() => { + myp5.buildGeometry(() => { + myp5.beginShape(myp5.TRIANGLES); + for (let i = 0; i < numTriangles; i++) { + myp5.vertex(0, 0, 0); + myp5.vertex(1, 0, 0); + myp5.vertex(0, 1, 0); + } + myp5.endShape(); + }); + }).not.toThrow(); + }); }); suite('fontWidth', function() { From b8e220830193210fe9ee89106da8985b4b500cdc Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 13 May 2026 10:42:16 -0400 Subject: [PATCH 211/250] Match filterRenderer2D premultiplied alpha to WebGL mode premultiplied alpha --- src/image/filterRenderer2D.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/image/filterRenderer2D.js b/src/image/filterRenderer2D.js index 40f0c52685..6188cdcb65 100644 --- a/src/image/filterRenderer2D.js +++ b/src/image/filterRenderer2D.js @@ -44,6 +44,7 @@ class FilterRenderer2D { console.error('WebGL not supported, cannot apply filter.'); return; } + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); this.textures = new Map(); From 86740584886dad0e213b73324b0c83339cca2cca Mon Sep 17 00:00:00 2001 From: perminder Date: Wed, 13 May 2026 20:24:07 +0530 Subject: [PATCH 212/250] removing-suggested-coverage --- .github/workflows/ci-test.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 484f424510..7f23015ba2 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -77,12 +77,6 @@ jobs: run: npm run test:types env: CI: true - - name: report test coverage - if: steps.test.outcome == 'success' - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 - with: - files: coverage/coverage-final.json - fail_ci_if_error: false - name: fail job if tests failed if: steps.test.outcome != 'success' run: exit 1 \ No newline at end of file From 76ba137a6d76b197a694a02ad4f8e8a997bd1c76 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 13 May 2026 13:34:39 -0400 Subject: [PATCH 213/250] Fix filters on p5.Graphics --- src/image/filterRenderer2D.js | 4 +-- test/unit/visual/cases/webgl.js | 33 +++++++++++++++++- .../000.png | Bin 0 -> 1250 bytes .../metadata.json | 3 ++ .../000.png | Bin 0 -> 1250 bytes .../metadata.json | 3 ++ .../000.png | Bin 0 -> 1457 bytes .../metadata.json | 3 ++ .../000.png | Bin 0 -> 1457 bytes .../metadata.json | 3 ++ test/unit/visual/visualTest.js | 9 +++-- 11 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGL/filter/Multiple filter passes work correctly on a p5.Graphics in 2d mode/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/Multiple filter passes work correctly on a p5.Graphics in 2d mode/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/Multiple filter passes work correctly on a p5.Graphics in webgl mode/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/Multiple filter passes work correctly on a p5.Graphics in webgl mode/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/Transparent background colors are correct in 2d mode/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/Transparent background colors are correct in 2d mode/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/Transparent background colors are correct in webgl mode/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/Transparent background colors are correct in webgl mode/metadata.json diff --git a/src/image/filterRenderer2D.js b/src/image/filterRenderer2D.js index 6188cdcb65..bd9da38d9b 100644 --- a/src/image/filterRenderer2D.js +++ b/src/image/filterRenderer2D.js @@ -44,7 +44,7 @@ class FilterRenderer2D { console.error('WebGL not supported, cannot apply filter.'); return; } - gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); this.textures = new Map(); @@ -382,7 +382,7 @@ class FilterRenderer2D { get canvasTexture() { if (!this._canvasTexture) { - this._canvasTexture = new Texture(this._renderer, this.parentRenderer.wrappedElt); + this._canvasTexture = new Texture(this._renderer, this.parentRenderer); } return this._canvasTexture; } diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 6e05ddc06f..fd0cb09d0a 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -272,6 +272,37 @@ visualSuite('WebGL', function() { screenshot(); }); + + for (const mode of ['webgl', '2d']) { + visualTest(`Transparent background colors are correct in ${mode} mode`, function(p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + const g = p5.createGraphics(50, 50, mode === 'webgl' ? p5.WEBGL : p5.P2D); + if (mode === 'webgl') g.translate(-p5.width/2, -p5.height/2); + g.noStroke(); + g.fill(255, 0, 0, 100); + g.rect(10, 10, 30, 30); + g.filter(p5.BLUR, 4); + p5.imageMode(p5.CENTER); + p5.image(g, 0, 0); + screenshot(); + }); + + visualTest(`Multiple filter passes work correctly on a p5.Graphics in ${mode} mode`, function(p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + const g = p5.createGraphics(50, 50, mode === 'webgl' ? p5.WEBGL : p5.P2D); + if (mode === 'webgl') g.translate(-g.width/2, -g.height/2); + g.background(255); + g.noStroke(); + g.fill(0); + g.rect(10, 10, 6, 6); + g.filter(p5.BLUR, 2); + g.rect(30, 30, 6, 6); + g.filter(p5.BLUR, 2); + p5.imageMode(p5.CENTER); + p5.image(g, 0, 0); + screenshot(); + }); + } }); visualSuite('Lights', function() { @@ -1164,7 +1195,7 @@ visualSuite('WebGL', function() { p5.plane(50, 50); screenshot(); }); - + visualSuite('auto-return for shader hooks', () => { visualTest('auto-returns input struct when return is omitted', (p5, screenshot) => { p5.createCanvas(50, 50, p5.WEBGL); diff --git a/test/unit/visual/screenshots/WebGL/filter/Multiple filter passes work correctly on a p5.Graphics in 2d mode/000.png b/test/unit/visual/screenshots/WebGL/filter/Multiple filter passes work correctly on a p5.Graphics in 2d mode/000.png new file mode 100644 index 0000000000000000000000000000000000000000..88030f07cc978b64ef993f822186189b9905e0f2 GIT binary patch literal 1250 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2o;fuuS)KaSW+od~-e1TP9Y3^?{c9 zZ>va7#l{+*AOGvS9=JOS7Tzzfy1^>bhAH3=el6^R3qSk?#227l)HRa{zWo2ba)z!Nl zbe6X@uXfk}_*EybG1%?S1coLKFMq5W!gALif^6V-gOV7bPJaX-fjm< zMMOt;U%h%2EcLG0AgN1qS=J#n-z6`F_wCsus3u|H&IKA@WgL}WbuAH=NUMZA%h(&1E)~%uQcpn|yHYphB(#6+J4`f~0{6nuW z&-K+=4y7K4-}U+V`Z)^&PXIN(TKRYF`RP)bx=*%!-+kr8jT;ePs`gfVdg5tQ`^#jW zeZ8E1+#U;7u;trDzij_|)FW-$zcW2&cz~YzaC39|$4^gBSN#1|x~KNH*}eVs@e0$x zcA6Jg%$1OtY5Al2`~PF-?kO4j_Xv2rm~Ecl^@CsD?#S_edF~@VK-S#<*J8?-8z#bV1Q{HT;^}n@$^bTfqc)zGY`W~oS7V|2n>5$?jy|+e!R=Czn(hp zjQ#bFw&ShZkN(Y!Y?*1aa2uZhUsAwf$-6-_osBPkut-0$*SOuuX!8t3Ar86L7}sa> z=j~{J?4(?hlJ+xx&9TrAH;dC2iR3*q(Ca)_uV)%mn)0mk#F^8NPra}DW>hJ->)ZUN zvDr^@4(HZ|KaA5m*!@i7$%0RLMeEbtR#_a{t6Tl``?r|;%Z?UhSxmiu)kj!4bV{bM z*OvU5U%!6+kjo5;=R3Eq9|cOZ2)HRMWLeL*@2|MHcye9cK4{9y7dJ6X+T`++$7LilB%k#mMvSxbp&YSj;b$q96+5Z_h)bMf7068s#I50WTd91#&sl^IT$Rz>sV>p ztTh~hEI0M!^3F3K+`M_Sp|No=G~%{%&yp|5V+V17N&Yvw+FUoy_e~o>i@gT-#E@tyY|et+H3_xJZF*VOERc&vTqnV#2u*5*zQX=29v z;%g?{^t!*VwtK#PJ=;gH>)vK5vAGK?H~$t<-q!6|E%xhgUCqBgmiy}dMuFv1T|hxI zY39@522h^7@i~DNZ1QZ&H`NAi94BSEHofQky3=P1yEHJY_QtK3IPbG|?u8WBRg=!> zF!@T)kZP4l;j7%b{`Ah369XM*I&Yl0{QTlMvyXgFd!nYayFk9TWM+`}xyM~w=9EpZ zwq55jU32S`Kl}^~|Nrao{JsDzsn|e86f=YMs^ptrD(2+?MHoC?{an^LB{Ts5*z#Y) literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/filter/Multiple filter passes work correctly on a p5.Graphics in 2d mode/metadata.json b/test/unit/visual/screenshots/WebGL/filter/Multiple filter passes work correctly on a p5.Graphics in 2d mode/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/Multiple filter passes work correctly on a p5.Graphics in 2d mode/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/Multiple filter passes work correctly on a p5.Graphics in webgl mode/000.png b/test/unit/visual/screenshots/WebGL/filter/Multiple filter passes work correctly on a p5.Graphics in webgl mode/000.png new file mode 100644 index 0000000000000000000000000000000000000000..88030f07cc978b64ef993f822186189b9905e0f2 GIT binary patch literal 1250 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2o;fuuS)KaSW+od~-e1TP9Y3^?{c9 zZ>va7#l{+*AOGvS9=JOS7Tzzfy1^>bhAH3=el6^R3qSk?#227l)HRa{zWo2ba)z!Nl zbe6X@uXfk}_*EybG1%?S1coLKFMq5W!gALif^6V-gOV7bPJaX-fjm< zMMOt;U%h%2EcLG0AgN1qS=J#n-z6`F_wCsus3u|H&IKA@WgL}WbuAH=NUMZA%h(&1E)~%uQcpn|yHYphB(#6+J4`f~0{6nuW z&-K+=4y7K4-}U+V`Z)^&PXIN(TKRYF`RP)bx=*%!-+kr8jT;ePs`gfVdg5tQ`^#jW zeZ8E1+#U;7u;trDzij_|)FW-$zcW2&cz~YzaC39|$4^gBSN#1|x~KNH*}eVs@e0$x zcA6Jg%$1OtY5Al2`~PF-?kO4j_Xv2rm~Ecl^@CsD?#S_edF~@VK-S#<*J8?-8z#bV1Q{HT;^}n@$^bTfqc)zGY`W~oS7V|2n>5$?jy|+e!R=Czn(hp zjQ#bFw&ShZkN(Y!Y?*1aa2uZhUsAwf$-6-_osBPkut-0$*SOuuX!8t3Ar86L7}sa> z=j~{J?4(?hlJ+xx&9TrAH;dC2iR3*q(Ca)_uV)%mn)0mk#F^8NPra}DW>hJ->)ZUN zvDr^@4(HZ|KaA5m*!@i7$%0RLMeEbtR#_a{t6Tl``?r|;%Z?UhSxmiu)kj!4bV{bM z*OvU5U%!6+kjo5;=R3Eq9|cOZ2)HRMWLeL*@2|MHcye9cK4{9y7dJ6X+T`++$7LilB%k#mMvSxbp&YSj;b$q96+5Z_h)bMf7068s#I50WTd91#&sl^IT$Rz>sV>p ztTh~hEI0M!^3F3K+`M_Sp|No=G~%{%&yp|5V+V17N&Yvw+FUoy_e~o>i@gT-#E@tyY|et+H3_xJZF*VOERc&vTqnV#2u*5*zQX=29v z;%g?{^t!*VwtK#PJ=;gH>)vK5vAGK?H~$t<-q!6|E%xhgUCqBgmiy}dMuFv1T|hxI zY39@522h^7@i~DNZ1QZ&H`NAi94BSEHofQky3=P1yEHJY_QtK3IPbG|?u8WBRg=!> zF!@T)kZP4l;j7%b{`Ah369XM*I&Yl0{QTlMvyXgFd!nYayFk9TWM+`}xyM~w=9EpZ zwq55jU32S`Kl}^~|Nrao{JsDzsn|e86f=YMs^ptrD(2+?MHoC?{an^LB{Ts5*z#Y) literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/filter/Multiple filter passes work correctly on a p5.Graphics in webgl mode/metadata.json b/test/unit/visual/screenshots/WebGL/filter/Multiple filter passes work correctly on a p5.Graphics in webgl mode/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/Multiple filter passes work correctly on a p5.Graphics in webgl mode/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/Transparent background colors are correct in 2d mode/000.png b/test/unit/visual/screenshots/WebGL/filter/Transparent background colors are correct in 2d mode/000.png new file mode 100644 index 0000000000000000000000000000000000000000..eb51606a3db9fbc5bec43450a1f477972c834f1e GIT binary patch literal 1457 zcmV;i1y1^jP)W2>429u#CbPf)qt3o0_$VC(Vw&wP%kh;{`uhMRhy%7fuA1b-_ROD8+xGMfA8P+K zIP}?``Ln!84L2IM^G_9boJC)IB-RUakFwpioq5K39u6iRx^rM|>tpqX z8IYN1sFH27IfCossWjO0CNzTPfg$tG&viEZQt=U~qrHb%@Ay#OFpHToA1#~9$A-uv z5?jwf>g|9DvkX8D{A(RVVtPccO{`b%&5I|@&}Ztij@V|+aQj#c_B8M|;Ozt^Ogjb% zN0+ddI@)`P^^Ona4YQaj^U>SjHj>x~*2NSW!Q0Us48a_8EMK7>_9?{B8@zbJ49HA0 zjQO)UlItS{_TWtfO&J7+p${QGV(*N#^rgJwOU;7JcO7@glMRv0S*_U*nrRM3dl&|L zdwop}`dW{9fM@)vnJ~}#)o-o2z#(X&X@}8@KGa^kI%mCmhf1{-{3J zciz9t&s^m0KqZe3eD-`kUPQ9i#EPDS&-tc9gvUnGYn7GQ*r(dxW`P@@QtC^RYf(MA6u&hj_-I)+qj~I@+;zhJHru3`IQXOj(YP z^$~dVi}a1e6q*NyGyhhzFz>DM)YHA1_)#;UY{oAcimdJak5x8l&}=p{z>0s$<%rce z>$4B<%t5R5-DTvri_8UbXc8DO!fMFBt1DLfSyzMPv+@AX_){~XY;KnsxgV_&*tsHy zgU~E!-c1b?{;Kmmmaph_iD~c6E56h$FdNG5_W9%@$!vxkpwJYI7$QUdSq&hk5Feoi zYAD3e8@zbJ3@~3*jI!TuKUyQHhmGKXH=z+U4-A=i{;2ce_ll259qm2DddG+IhFQ#% z`Doc(KKVv0!YXwj2dTFMCd@JbHSlkB5Q*s#y*9C4=}md@gclh>)UBX`KXzwA`J3f>*a+%CW%{=HAu@PKCOra6H9nHZI%rVFE73yK1 zLJYmZizm#0%tXWGB2VL=tVf(j3LJzt5j15G7=}KC_=vqT*3y^qiZ3+_GGFHPZ-$rO zTJ1856i_%AG}9c6_Am_g_WGI{^tB%I0MGbSGa)mMOGx^UmK?oM3{5ocFj~=v+KX4` ztalGFZ!?5Gqj+$;Nc&c7I209Y7BrtRK$!PddFtt2P5h`CkeTT1+mPHYBH6i&!<{sx zFaYgXJ3~Jsc7}pyRNOJ@-6bsCUBrURKzyY)5i@`o;@3bgq?foe#xWa4(jH%3BzuaH zytFqJuMF+IgjK(?-o`gD&9n7Cp8)^>|NpB&Eyn-=00v1!K~w_(bCYW2>429u#CbPf)qt3o0_$VC(Vw&wP%kh;{`uhMRhy%7fuA1b-_ROD8+xGMfA8P+K zIP}?``Ln!84L2IM^G_9boJC)IB-RUakFwpioq5K39u6iRx^rM|>tpqX z8IYN1sFH27IfCossWjO0CNzTPfg$tG&viEZQt=U~qrHb%@Ay#OFpHToA1#~9$A-uv z5?jwf>g|9DvkX8D{A(RVVtPccO{`b%&5I|@&}Ztij@V|+aQj#c_B8M|;Ozt^Ogjb% zN0+ddI@)`P^^Ona4YQaj^U>SjHj>x~*2NSW!Q0Us48a_8EMK7>_9?{B8@zbJ49HA0 zjQO)UlItS{_TWtfO&J7+p${QGV(*N#^rgJwOU;7JcO7@glMRv0S*_U*nrRM3dl&|L zdwop}`dW{9fM@)vnJ~}#)o-o2z#(X&X@}8@KGa^kI%mCmhf1{-{3J zciz9t&s^m0KqZe3eD-`kUPQ9i#EPDS&-tc9gvUnGYn7GQ*r(dxW`P@@QtC^RYf(MA6u&hj_-I)+qj~I@+;zhJHru3`IQXOj(YP z^$~dVi}a1e6q*NyGyhhzFz>DM)YHA1_)#;UY{oAcimdJak5x8l&}=p{z>0s$<%rce z>$4B<%t5R5-DTvri_8UbXc8DO!fMFBt1DLfSyzMPv+@AX_){~XY;KnsxgV_&*tsHy zgU~E!-c1b?{;Kmmmaph_iD~c6E56h$FdNG5_W9%@$!vxkpwJYI7$QUdSq&hk5Feoi zYAD3e8@zbJ3@~3*jI!TuKUyQHhmGKXH=z+U4-A=i{;2ce_ll259qm2DddG+IhFQ#% z`Doc(KKVv0!YXwj2dTFMCd@JbHSlkB5Q*s#y*9C4=}md@gclh>)UBX`KXzwA`J3f>*a+%CW%{=HAu@PKCOra6H9nHZI%rVFE73yK1 zLJYmZizm#0%tXWGB2VL=tVf(j3LJzt5j15G7=}KC_=vqT*3y^qiZ3+_GGFHPZ-$rO zTJ1856i_%AG}9c6_Am_g_WGI{^tB%I0MGbSGa)mMOGx^UmK?oM3{5ocFj~=v+KX4` ztalGFZ!?5Gqj+$;Nc&c7I209Y7BrtRK$!PddFtt2P5h`CkeTT1+mPHYBH6i&!<{sx zFaYgXJ3~Jsc7}pyRNOJ@-6bsCUBrURKzyY)5i@`o;@3bgq?foe#xWa4(jH%3BzuaH zytFqJuMF+IgjK(?-o`gD&9n7Cp8)^>|NpB&Eyn-=00v1!K~w_(bCY { let lastShiftThreshold; let lastPrefix; - let lastDeviceRatio = window.devicePixelRatio; beforeAll(() => { lastPrefix = namePrefix; namePrefix += escapeName(name) + '/'; @@ -72,16 +71,12 @@ export function visualSuite( if (newShiftThreshold !== undefined) { shiftThreshold = newShiftThreshold; } - - // Force everything to be 1x - window.devicePixelRatio = 1; }); callback(); afterAll(() => { namePrefix = lastPrefix; - window.devicePixelRatio = lastDeviceRatio; shiftThreshold = lastShiftThreshold; }); }); @@ -398,9 +393,12 @@ export function visualTest( suiteFn(testName, function() { let name; let myp5; + let lastDeviceRatio = window.devicePixelRatio; beforeAll(function() { name = namePrefix + escapeName(testName); + // Force everything to be 1x + window.devicePixelRatio = 1; return new Promise(res => { myp5 = new p5(function(p) { p.setup = function() { @@ -411,6 +409,7 @@ export function visualTest( }); afterAll(function() { + window.devicePixelRatio = lastDeviceRatio; myp5.remove(); }); From 62143fc9862c2cb9d08ecfb374a0c1737c20fec6 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 13 May 2026 14:14:14 -0400 Subject: [PATCH 214/250] Fix p5.strands hook params clashing with user uniform names --- src/strands/ir_types.js | 1 + src/strands/strands_api.js | 9 +++++---- src/webgl/strands_glslBackend.js | 4 ++-- src/webgpu/strands_wgslBackend.js | 8 ++++---- test/unit/webgl/p5.RendererGL.js | 27 +++++++++++++++++++++++++++ test/unit/webgpu/p5.RendererWebGPU.js | 26 ++++++++++++++++++++++++++ 6 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 8bede07242..20a432ffdc 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -12,6 +12,7 @@ export const NodeType = { ASSIGNMENT: 'assignment', }; export const INSTANCE_ID_VARYING_NAME = '_p5_instanceID'; +export const HOOK_PARAM_PREFIX = '_p5_param_'; export const NodeTypeToName = Object.fromEntries( Object.entries(NodeType).map(([key, val]) => [val, key]) ); diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 69bb2f4179..8675dc69b6 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -10,6 +10,7 @@ import { OpCode, StatementType, NodeType, + HOOK_PARAM_PREFIX, // isNativeType } from './ir_types' import { strandsBuiltinFunctions } from './strands_builtins' @@ -647,7 +648,7 @@ function createHookArguments(strandsContext, parameters){ for (const param of parameters) { if(isStructType(param.type)) { const structTypeInfo = structType(param); - const { id, dimension } = build.structInstanceNode(strandsContext, structTypeInfo, param.name, []); + const { id, dimension } = build.structInstanceNode(strandsContext, structTypeInfo, `${HOOK_PARAM_PREFIX}${param.name}`, []); const structNode = createStrandsNode(id, dimension, strandsContext).withStructProperties( structTypeInfo.properties.map(prop => prop.name) ); @@ -660,7 +661,7 @@ function createHookArguments(strandsContext, parameters){ const oldDeps = dag.dependsOn[structNode.id]; const newDeps = oldDeps.slice(); newDeps[i] = newFieldID; - const rebuilt = build.structInstanceNode(strandsContext, structTypeInfo, param.name, newDeps); + const rebuilt = build.structInstanceNode(strandsContext, structTypeInfo, `${HOOK_PARAM_PREFIX}${param.name}`, newDeps); structNode.id = rebuilt.id; }; // TODO: implement member access operations @@ -681,7 +682,7 @@ function createHookArguments(strandsContext, parameters){ newValueID = newVal.id; } newDependsOn[i] = newValueID; - const newStructInfo = build.structInstanceNode(strandsContext, structTypeInfo, param.name, newDependsOn); + const newStructInfo = build.structInstanceNode(strandsContext, structTypeInfo, `${HOOK_PARAM_PREFIX}${param.name}`, newDependsOn); structNode.id = newStructInfo.id; } }) @@ -697,7 +698,7 @@ function createHookArguments(strandsContext, parameters){ throw new Error(`Missing dataType for parameter ${param.name} of type ${param.type.typeName}`); } const typeInfo = param.type.dataType; - const { id, dimension } = build.variableNode(strandsContext, typeInfo, param.name); + const { id, dimension } = build.variableNode(strandsContext, typeInfo, `${HOOK_PARAM_PREFIX}${param.name}`); const arg = createStrandsNode(id, dimension, strandsContext); args.push(arg); } diff --git a/src/webgl/strands_glslBackend.js b/src/webgl/strands_glslBackend.js index 0876547a89..bbd05a5950 100644 --- a/src/webgl/strands_glslBackend.js +++ b/src/webgl/strands_glslBackend.js @@ -1,7 +1,7 @@ import noiseGLSL from './shaders/functions/noise3DGLSL.glsl'; import randomGLSL from './shaders/functions/randomGLSL.glsl'; import randomVertGLSL from './shaders/functions/randomVertGLSL.glsl'; -import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType, DataType, INSTANCE_ID_VARYING_NAME } from "../strands/ir_types"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType, DataType, INSTANCE_ID_VARYING_NAME, HOOK_PARAM_PREFIX } from "../strands/ir_types"; import { getNodeDataFromID, extractNodeTypeInfo } from "../strands/ir_dag"; import * as FES from '../strands/strands_FES'; import * as build from '../strands/ir_builders'; @@ -168,7 +168,7 @@ const cfgHandlers = { export const glslBackend = { hookEntry(hookType) { const firstLine = `(${hookType.parameters.flatMap((param) => { - return `${param.qualifiers?.length ? param.qualifiers.join(' ') : ''}${param.type.typeName} ${param.name}`; + return `${param.qualifiers?.length ? param.qualifiers.join(' ') : ''}${param.type.typeName} ${HOOK_PARAM_PREFIX}${param.name}`; }).join(', ')}) {`; return firstLine; }, diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index 59aedde92c..79d0f2816b 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -2,7 +2,7 @@ import noiseWGSL from './shaders/functions/noise3DWGSL.js'; import randomWGSL from './shaders/functions/randomWGSL'; import randomVertWGSL from './shaders/functions/randomVertWGSL'; import randomComputeWGSL from './shaders/functions/randomComputeWGSL'; -import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType, DataType, INSTANCE_ID_VARYING_NAME } from "../strands/ir_types"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType, DataType, INSTANCE_ID_VARYING_NAME, HOOK_PARAM_PREFIX } from "../strands/ir_types"; import { getNodeDataFromID, extractNodeTypeInfo } from "../strands/ir_dag"; import * as FES from '../strands/strands_FES'; import * as build from '../strands/ir_builders'; @@ -190,16 +190,16 @@ export const wgslBackend = { hookEntry(hookType) { const params = hookType.parameters.map((param) => { // For struct types, use a raw prefix since we'll create a mutable copy - const paramName = param.type.properties ? `_p5_strands_raw_${param.name}` : param.name; + const paramName = param.type.properties ? `_p5_strands_raw_${param.name}` : `${HOOK_PARAM_PREFIX}${param.name}`; return `${paramName}: ${param.type.typeName}`; }).join(', '); const firstLine = `(${params}) {`; - // Generate mutable copies for struct parameters with original names + // Generate mutable copies for struct parameters const mutableCopies = hookType.parameters .filter(param => param.type.properties) // Only struct types - .map(param => ` var ${param.name} = _p5_strands_raw_${param.name};`) + .map(param => ` var ${HOOK_PARAM_PREFIX}${param.name} = _p5_strands_raw_${param.name};`) .join('\n'); return mutableCopies ? firstLine + '\n' + mutableCopies : firstLine; diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index aaa229bcec..93582483b6 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -89,6 +89,33 @@ suite('p5.RendererGL', function() { }); }); + suite('p5.strands', function() { + test('a uniform whose name matches a hook parameter name does not break', function() { + myp5.createCanvas(10, 10, myp5.WEBGL); + myp5.pixelDensity(1); + + // 'color' is the GLSL parameter name of the getFinalColor hook's first argument. + // Creating a uniform with the same name used to cause a GLSL name clash. + const myShader = myp5.baseColorShader().modify(() => { + const color = myp5.uniformFloat('color', 0.5); + myp5.finalColor.begin(); + myp5.finalColor.set([color, color, color, 1]); + myp5.finalColor.end(); + }, { myp5 }); + + myp5.background(0); + myp5.noStroke(); + myp5.shader(myShader); + myp5.plane(myp5.width, myp5.height); + + const pixel = myp5.get(5, 5); + assert.approximately(pixel[0], 128, 1); + assert.equal(pixel[0], pixel[1]); + assert.equal(pixel[1], pixel[2]); + assert.equal(pixel[3], 255); + }); + }); + suite('texture binding', function() { test('setting a custom texture works', function() { myp5.createCanvas(10, 10, myp5.WEBGL); diff --git a/test/unit/webgpu/p5.RendererWebGPU.js b/test/unit/webgpu/p5.RendererWebGPU.js index f21503be60..3a21f174f6 100644 --- a/test/unit/webgpu/p5.RendererWebGPU.js +++ b/test/unit/webgpu/p5.RendererWebGPU.js @@ -321,4 +321,30 @@ suite('WebGPU p5.RendererWebGPU', function() { expect(() => buf.set(0, 42)).to.throw(); }); }); + + suite('p5.strands', function() { + test('a uniform whose name matches a hook parameter name does not break', async function() { + myp5.pixelDensity(1); + + // 'color' is the WGSL parameter name of the getFinalColor hook's first argument. + // Creating a uniform with the same name used to cause a WGSL name clash. + const myShader = myp5.baseColorShader().modify(() => { + const color = myp5.uniformFloat('color', 0.5); + myp5.finalColor.begin(); + myp5.finalColor.set([color, color, color, 1]); + myp5.finalColor.end(); + }, { myp5 }); + + myp5.background(0); + myp5.noStroke(); + myp5.shader(myShader); + myp5.plane(myp5.width, myp5.height); + + const pixel = await myp5.get(5, 5); + expect(pixel[0]).to.be.closeTo(128, 1); + expect(pixel[0]).to.equal(pixel[1]); + expect(pixel[1]).to.equal(pixel[2]); + expect(pixel[3]).to.equal(255); + }); + }); }); From dbdc1335db1f772c819e886fe9b4c805f2db6652 Mon Sep 17 00:00:00 2001 From: perminder Date: Fri, 15 May 2026 15:08:38 +0530 Subject: [PATCH 215/250] adding-missing-doubleClicked-handler --- src/webgl/light.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/webgl/light.js b/src/webgl/light.js index 483b9a53b7..1088c909e1 100644 --- a/src/webgl/light.js +++ b/src/webgl/light.js @@ -498,6 +498,11 @@ function light(p5, fn){ * sphere(30); * } * + * function doubleClicked() { + * isLit = !isLit; + * return false; + * } + * * @example * // Click and drag the mouse to view the scene from different angles. * From 71602423302ad78cd0f244e97401b2d30866150b Mon Sep 17 00:00:00 2001 From: kit Date: Fri, 15 May 2026 20:26:36 +0200 Subject: [PATCH 216/250] 2.3.0-rc.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6b8b1d8aff..fcfeaeb678 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "p5", - "version": "2.3.0-rc.3", + "version": "2.3.0-rc.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "p5", - "version": "2.3.0-rc.3", + "version": "2.3.0-rc.4", "license": "LGPL-2.1", "dependencies": { "@davepagurek/bezier-path": "^0.0.7", diff --git a/package.json b/package.json index 24045f6592..5993cbfb3b 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test/**/*.js": "eslint", "utils/**/*.{js,mjs}": "eslint" }, - "version": "2.3.0-rc.3", + "version": "2.3.0-rc.4", "dependencies": { "@davepagurek/bezier-path": "^0.0.7", "@japont/unicode-range": "^1.0.0", From aaf35c519f2dda1d297161ca97ff326c58620782 Mon Sep 17 00:00:00 2001 From: SOUMITRO-SAHA Date: Sat, 16 May 2026 13:23:29 +0530 Subject: [PATCH 217/250] Add shader examples to noise random and millis functions Add practical examples showing how noise random and millis can be used with p5.strands shader system including complete working code examples for cloud effects random colors and time-based transitions --- src/math/noise.js | 26 ++++++++++++++++++++++++++ src/math/random.js | 29 +++++++++++++++++++++++++++++ src/utilities/time_date.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/src/math/noise.js b/src/math/noise.js index 16c08c484c..0b6444a6c9 100644 --- a/src/math/noise.js +++ b/src/math/noise.js @@ -227,6 +227,32 @@ function noise(p5, fn){ * } * } * } + * + * `noise()` can also be used in shaders with p5.strands. The following example + * uses `noise()` to create a cloud-like texture effect in a filter shader. + * + * ```js example + * let myFilter; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * myFilter = buildFilterShader(shaderCallback); + * describe('A cloud-like noise pattern.'); + * } + * + * function shaderCallback() { + * filterColor.begin(); + * let coord = filterColor.texCoord; + * let t = millis() / 2000; + * let value = noise(coord.x * 5, coord.y * 5, t); + * filterColor.set(mix([0.1, 0.1, 0.3, 1], [0.9, 0.9, 1, 1], value)); + * filterColor.end(); + * } + * + * function draw() { + * filter(myFilter); + * } + * ``` */ fn.noise = function(x, y = 0, z = 0) { if (perlin == null) { diff --git a/src/math/random.js b/src/math/random.js index cd56ed5530..f6bc21eb3d 100644 --- a/src/math/random.js +++ b/src/math/random.js @@ -230,6 +230,35 @@ function random(p5, fn){ * // Draw the point. * point(x, y); * } + * + * `random()` can also be used in shaders with p5.strands. The following example + * uses `random()` to create varying colors on a shape. + * + * ```js example + * let myShader; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * myShader = buildColorShader(shaderCallback); + * describe('A sphere with randomly varying colors.'); + * } + * + * function shaderCallback() { + * let r = random(); + * let g = random(); + * let b = random(); + * finalColor.begin(); + * finalColor.set([r, g, b, 1]); + * finalColor.end(); + * } + * + * function draw() { + * background(220); + * shader(myShader); + * noStroke(); + * sphere(30); + * } + * ``` */ /** * @method random diff --git a/src/utilities/time_date.js b/src/utilities/time_date.js index 029b36a7cc..68aabfbc08 100644 --- a/src/utilities/time_date.js +++ b/src/utilities/time_date.js @@ -198,6 +198,34 @@ function timeDate(p5, fn){ * `The text "It took ${round(ms, 2)} ms to load the data" written in black on a gray background.` * ); * } + * + * `millis()` can also be used in shaders with p5.strands. The following example + * uses `millis()` to create time-based color transitions on a shape. + * + * ```js example + * let myShader; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * myShader = buildColorShader(shaderCallback); + * describe('A sphere whose color shifts over time.'); + * } + * + * function shaderCallback() { + * let t = millis() * 0.001; + * let value = 0.5 + 0.5 * sin(t); + * finalColor.begin(); + * finalColor.set(mix([0.2, 0.6, 0.8, 1], [0.8, 0.2, 0.6, 1], value)); + * finalColor.end(); + * } + * + * function draw() { + * background(220); + * shader(myShader); + * noStroke(); + * sphere(30); + * } + * ``` */ fn.millis = function() { if (this._millisStart === -1) { From 18f0808bf847494fd766c2615a37d646afaa3bd9 Mon Sep 17 00:00:00 2001 From: SOUMITRO-SAHA Date: Mon, 18 May 2026 15:58:37 +0530 Subject: [PATCH 218/250] - Use named color variables in examples --- src/math/noise.js | 6 ++++-- src/utilities/time_date.js | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/math/noise.js b/src/math/noise.js index 0b6444a6c9..82b3f1f2cb 100644 --- a/src/math/noise.js +++ b/src/math/noise.js @@ -244,8 +244,10 @@ function noise(p5, fn){ * filterColor.begin(); * let coord = filterColor.texCoord; * let t = millis() / 2000; - * let value = noise(coord.x * 5, coord.y * 5, t); - * filterColor.set(mix([0.1, 0.1, 0.3, 1], [0.9, 0.9, 1, 1], value)); + * let mixFraction = noise(coord.x * 5, coord.y * 5, t); + * let darkBlue = [0.1, 0.1, 0.3, 1]; + * let lightBlue = [0.9, 0.9, 1, 1]; + * filterColor.set(mix(darkBlue, lightBlue, mixFraction)); * filterColor.end(); * } * diff --git a/src/utilities/time_date.js b/src/utilities/time_date.js index 68aabfbc08..5b85c4dcbb 100644 --- a/src/utilities/time_date.js +++ b/src/utilities/time_date.js @@ -214,8 +214,10 @@ function timeDate(p5, fn){ * function shaderCallback() { * let t = millis() * 0.001; * let value = 0.5 + 0.5 * sin(t); + * let skyBlue = [0.2, 0.6, 0.8, 1]; + * let magenta = [0.8, 0.2, 0.6, 1]; * finalColor.begin(); - * finalColor.set(mix([0.2, 0.6, 0.8, 1], [0.8, 0.2, 0.6, 1], value)); + * finalColor.set(mix(skyBlue, magenta, value)); * finalColor.end(); * } * From b69210df2c09543cefbd4beae7ad8a25068a9c6f Mon Sep 17 00:00:00 2001 From: SOUMITRO-SAHA Date: Mon, 18 May 2026 16:49:26 +0530 Subject: [PATCH 219/250] Add noise() return range comment per review feedback --- src/math/noise.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/math/noise.js b/src/math/noise.js index 82b3f1f2cb..2752115494 100644 --- a/src/math/noise.js +++ b/src/math/noise.js @@ -228,8 +228,9 @@ function noise(p5, fn){ * } * } * - * `noise()` can also be used in shaders with p5.strands. The following example - * uses `noise()` to create a cloud-like texture effect in a filter shader. + * `noise()` can also be used in shaders with p5.strands, where it returns + * values in the range 0 to 1. The following example uses `noise()` to create + * a cloud-like texture effect in a filter shader. * * ```js example * let myFilter; @@ -244,6 +245,7 @@ function noise(p5, fn){ * filterColor.begin(); * let coord = filterColor.texCoord; * let t = millis() / 2000; + * // noise() returns values in the range 0 to 1. * let mixFraction = noise(coord.x * 5, coord.y * 5, t); * let darkBlue = [0.1, 0.1, 0.3, 1]; * let lightBlue = [0.9, 0.9, 1, 1]; From 1d5af24de19f9075c42eac0a3ef067529cf39ee6 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 19 May 2026 08:54:50 -0400 Subject: [PATCH 220/250] Vector perf tests --- src/math/math.js | 10 ++++++++- src/math/p5.Vector.js | 50 +++++++++++++++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/math/math.js b/src/math/math.js index 5a0c452c4b..0c0d15c870 100644 --- a/src/math/math.js +++ b/src/math/math.js @@ -92,7 +92,15 @@ function math(p5, fn) { * } */ fn.createVector = function (...args) { - return new p5.Vector(...args); + if (this._boundFromRadians) { + this._boundFromRadians = this._fromRadians.bind(this); + this._boundToRadians = this._toRadians.bind(this); + } + if (this instanceof p5) { + return new p5.Vector(this._boundFromRadians, this._boundToRadians, ...args); + } else { + return new p5.Vector(...args); + } }; /** diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 0bc152162b..17ae3162f3 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -5,6 +5,7 @@ import * as constants from '../core/constants'; /** + * @private * This function is used by binary vector operations to prioritize shorter vectors, * and to emit a warning when lengths do not match. */ @@ -12,6 +13,16 @@ const prioritizeSmallerDimension = function(currentVectorDimension, args) { return Math.min(currentVectorDimension, args.length); }; +/** + * @private + * In-place, shrinks an array to a dimension. + */ +const shrinkToDimension = function(arr, dim) { + while (arr.length > dim) { + arr.pop(); + } +} + class Vector { /** @@ -41,6 +52,15 @@ class Vector { */ values = []; + /** + * @private + * Check for disabled friendly errors. + * This is overridden in the addon function to check the p5 instance. + */ + static friendlyErrorsDisabled() { + return true; + } + // This is how it comes in with createVector() // This check if the first argument is a function constructor(...args) { @@ -59,11 +79,16 @@ class Vector { } this.values = []; - if(Array.isArray(args) && !args.every(v => typeof v === 'number' && Number.isFinite(v))){ - this._friendlyError( - 'Arguments contain non-finite numbers', - 'p5.Vector' - ); + if (!Vector.friendlyErrorsDisabled() && Array.isArray(args)) { + for (let i = 0; i < args.length; i++) { + const v = args[i]; + if (typeof v !== 'number' || !Number.isFinite(v)) { + this._friendlyError( + 'Arguments contain non-finite numbers', + 'p5.Vector' + ); + } + } } else { this.values = args; } @@ -759,11 +784,11 @@ class Vector { */ sub(...args) { const minDimension = prioritizeSmallerDimension(this.dimensions, args); + shrinkToDimension(this.values, minDimension); - this.values = this.values.reduce((acc, v, i) => { - if(i < minDimension) acc[i] = this.values[i] - args[i]; - return acc; - }, new Array(minDimension)); + for (let i = 0; i < this.values.length; i++) { + this.values[i] -= args[i]; + } return this; } @@ -1372,7 +1397,7 @@ class Vector { * The cross product is a vector that points straight out of the plane created * by two vectors. The cross product's magnitude is the area of the parallelogram * formed by the original two vectors. - * + * * The cross product is defined on 3-dimensional vectors, and will use the `x`, `y`, * and `z` components. This method should only be used with 3D vectors. * @@ -1986,7 +2011,7 @@ class Vector { if (this.dimensions < 2 || ( this._values instanceof Array && this._values.slice(2).some(v => v !== 0)) ) { - p5._friendlyError( + p5._friendlyError( 'p5.Vector.setHeading() only supports 2D vectors (z === 0). ' + 'For 3D or higher-dimensional vectors, use rotate() or another ' + 'appropriate method instead.', @@ -3617,6 +3642,9 @@ function vector(p5, fn) { p5.Vector = Vector; Vector.prototype._friendlyError = p5._friendlyError; + Vector.prototype.friendlyErrorsDisabled = function() { + return p5.disableFriendlyErrors; + }; /** * The x component of the vector From 4d7abf53026d215a665e1b33a32b857da062b4fe Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 19 May 2026 09:08:52 -0400 Subject: [PATCH 221/250] Turn magSq into a loop too --- src/math/math.js | 2 +- src/math/p5.Vector.js | 10 ++++++---- src/math/patch-vector.js | 21 ++++++++++++++------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/math/math.js b/src/math/math.js index 0c0d15c870..2e4184962c 100644 --- a/src/math/math.js +++ b/src/math/math.js @@ -92,7 +92,7 @@ function math(p5, fn) { * } */ fn.createVector = function (...args) { - if (this._boundFromRadians) { + if (!this._boundFromRadians) { this._boundFromRadians = this._fromRadians.bind(this); this._boundToRadians = this._toRadians.bind(this); } diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 17ae3162f3..0dc0f9c1e4 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -1280,10 +1280,12 @@ class Vector { * } */ magSq() { - return this.values.reduce( - (sum, component) => sum + component * component, - 0 - ); + let sum = 0; + for (let i = 0; i < this.values.length; i++) { + const component = this.values[i]; + sum += component * component; + } + return sum; } /** diff --git a/src/math/patch-vector.js b/src/math/patch-vector.js index d615027d07..8bc549be98 100644 --- a/src/math/patch-vector.js +++ b/src/math/patch-vector.js @@ -43,13 +43,20 @@ export function _validatedVectorOperation(expectsSoloNumberArgument){ args = new Array(3).fill(args[0]); } - if(Array.isArray(args) && !args.every(v => typeof v === 'number' && Number.isFinite(v))){ - this._friendlyError( - 'Arguments contain non-finite numbers', - target.name - ); - return this; - }; + if (!Vector.friendlyErrorsDisabled() && Array.isArray(args)) { + for (let i = 0; i < args.length; i++) { + const v = args[i]; + if (typeof v !== 'number' || !Number.isFinite(v)) { + this._friendlyError( + 'Arguments contain non-finite numbers', + 'p5.Vector' + ); + } + return this; + } + } else { + this.values = args; + } return target.call(this, ...args); }; From e7e5dde1e8c7f3fa409d67125e802bfa4b194d94 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 19 May 2026 09:12:40 -0400 Subject: [PATCH 222/250] Remove copy-and-pasted branch --- src/math/patch-vector.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/math/patch-vector.js b/src/math/patch-vector.js index 8bc549be98..1aa65d0705 100644 --- a/src/math/patch-vector.js +++ b/src/math/patch-vector.js @@ -54,8 +54,6 @@ export function _validatedVectorOperation(expectsSoloNumberArgument){ } return this; } - } else { - this.values = args; } return target.call(this, ...args); From e04141b17e03002c3c217e6c42e0a7eee2c427fb Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 19 May 2026 09:20:16 -0400 Subject: [PATCH 223/250] Avoid a few new arrays --- src/math/p5.Vector.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 0dc0f9c1e4..593bfd114f 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -73,12 +73,11 @@ class Vector { if (typeof args[0] === 'function') { this.isPInst = true; - this._fromRadians = args[0]; - this._toRadians = args[1]; - args = args.slice(2); + this._fromRadians = args.shift(); + this._toRadians = args.shift(); } - this.values = []; + this.values = args; if (!Vector.friendlyErrorsDisabled() && Array.isArray(args)) { for (let i = 0; i < args.length; i++) { const v = args[i]; @@ -88,9 +87,9 @@ class Vector { 'p5.Vector' ); } + this.values = []; + break; } - } else { - this.values = args; } // This property is here where duck typing (checking if obj.isVector) needs From 0d04fbb54b1e2257c721a870e9f8016d80f486ab Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 19 May 2026 09:41:03 -0400 Subject: [PATCH 224/250] Attempt alternate dist() implementation --- src/math/p5.Vector.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 593bfd114f..9397c0d690 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -1547,7 +1547,12 @@ class Vector { * } */ dist(v) { - return v.copy().sub(this).mag(); + const minDimension = prioritizeSmallerDimension(this.dimensions, v.values); + const components = this.values.slice(0, minDimension); + for (let i = 0; i < minDimension; i++) { + components[i] -= v.values[i]; + } + return Math.hypot(...components); } /** From 0bd50d2b7dbf10a25c19e52bd357c2d4417978c9 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 19 May 2026 09:48:30 -0400 Subject: [PATCH 225/250] Attempt to short circuit complex code when FES disabled --- src/core/friendly_errors/param_validator.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/friendly_errors/param_validator.js b/src/core/friendly_errors/param_validator.js index ed822db3cf..6277304168 100644 --- a/src/core/friendly_errors/param_validator.js +++ b/src/core/friendly_errors/param_validator.js @@ -606,6 +606,9 @@ function validateParams(p5, fn, lifecycles) { function(target, { kind, name }){ if(kind === 'method'){ return function(...args){ + if (p5.disableFriendlyErrors) { + return target.apply(this, args); + } const wasInternalCall = this._isUserCall; this._isUserCall = true; try { From a84c47918b9d6af581a0d5b5f2218fb0be987c12 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 19 May 2026 09:52:36 -0400 Subject: [PATCH 226/250] Attempt a dist() without a new array --- src/math/p5.Vector.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 9397c0d690..67aff10978 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -1548,11 +1548,12 @@ class Vector { */ dist(v) { const minDimension = prioritizeSmallerDimension(this.dimensions, v.values); - const components = this.values.slice(0, minDimension); + let sum = 0; for (let i = 0; i < minDimension; i++) { - components[i] -= v.values[i]; + const component = this.values[i] - v.values[i]; + sum += component * component; } - return Math.hypot(...components); + return Math.sqrt(sum); } /** From ebff602aabf22a322f82f317292a4cd311d798f4 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 19 May 2026 10:03:52 -0400 Subject: [PATCH 227/250] Remove intermediate arrays in more methods --- src/math/p5.Vector.js | 108 ++++++++++++++++-------------------------- 1 file changed, 42 insertions(+), 66 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 67aff10978..4f295bc1f2 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -527,11 +527,11 @@ class Vector { */ add(...args) { const minDimension = prioritizeSmallerDimension(this.dimensions, args); + shrinkToDimension(this.values, minDimension); - this.values = this.values.reduce((acc, v, i) => { - if(i < minDimension) acc[i] = this.values[i] + Number(args[i]); - return acc; - }, new Array(minDimension)); + for (let i = 0; i < this.values.length; i++) { + this.values[i] += args[i]; + } return this; } @@ -651,10 +651,11 @@ class Vector { */ rem(...args) { const minDimension = prioritizeSmallerDimension(this.dimensions, args); + shrinkToDimension(this.values, minDimension); - this.values = Array.from({ length: minDimension }, (_, i) => { - return (args[i] > 0) ? this.values[i] % args[i] : this.values[i]; - }); + for (let i = 0; i < this.values.length; i++) { + this.values[i] = this.values[i] % args[i]; + } return this; } @@ -971,50 +972,12 @@ class Vector { */ mult(...args) { const minDimension = prioritizeSmallerDimension(this.dimensions, args); + shrinkToDimension(this.values, minDimension); + + for (let i = 0; i < this.values.length; i++) { + this.values[i] *= args[i]; + } - this.values = this.values.reduce((acc, v, i) => { - if(i < minDimension) acc[i] = this.values[i] * args[i]; - return acc; - }, new Array(minDimension)); - - // if (args.length === 1 && args[0] instanceof Vector) { - // const v = args[0]; - // const maxLen = Math.min(this.values.length, v.values.length); - // for (let i = 0; i < maxLen; i++) { - // if (Number.isFinite(v.values[i]) && typeof v.values[i] === 'number') { - // if(!this.values[i]) this.values[i] = 0; - // this.values[i] *= v.values[i]; - // } else { - // console.warn( - // 'p5.Vector.prototype.mult:', - // 'v contains components that are either undefined or not finite numbers' - // ); - // return this; - // } - // } - // } else if (args.length === 1 && Array.isArray(args[0])) { - // const arr = args[0]; - // const maxLen = Math.min(this.values.length, arr.length); - // for (let i = 0; i < maxLen; i++) { - // if (Number.isFinite(arr[i]) && typeof arr[i] === 'number') { - // this.values[i] *= arr[i]; - // } else { - // console.warn( - // 'p5.Vector.prototype.mult:', - // 'arr contains elements that are either undefined or not finite numbers' - // ); - // return this; - // } - // } - // } else if ( - // args.length === 1 && - // typeof args[0] === 'number' && - // Number.isFinite(args[0]) - // ) { - // for (let i = 0; i < this.values.length; i++) { - // this.values[i] *= args[0]; - // } - // } return this; } @@ -1198,19 +1161,23 @@ class Vector { */ div(...args) { const minDimension = prioritizeSmallerDimension(this.dimensions, args); + shrinkToDimension(this.values, minDimension); - if(!args.every(v => typeof v === 'number' && v !== 0)){ - console.warn( - 'p5.Vector.prototype.div', - 'Arguments contain components that are 0' - ); - return this; - }; + if (!this.friendlyErrorsDisabled()) { + for (let i = 0; i < this.values.length; i++) { + if (typeof args[i] !== 'number' || args[i] === 0) { + console.warn( + 'p5.Vector.prototype.div', + 'Arguments contain components that are 0' + ); + return this; + } + } + } - this.values = this.values.reduce((acc, v, i) => { - if(i < minDimension) acc[i] = this.values[i] / args[i]; - return acc; - }, new Array(minDimension)); + for (let i = 0; i < this.values.length; i++) { + this.values[i] *= args[i]; + } return this; } @@ -1247,7 +1214,12 @@ class Vector { * } */ mag() { - return Math.sqrt(this.magSq()); + let sum = 0; + for (let i = 0; i < this.values.length; i++) { + const component = this.values[i]; + sum += component * component; + } + return Math.sqrt(sum); } /** @@ -1384,12 +1356,16 @@ class Vector { * @return {Number} */ dot(...args) { + let vals = args; if (args[0] instanceof Vector) { - return this.dot(...args[0].values); + vals = args[0].values; } - return this.values.reduce((sum, component, index) => { - return sum + component * (args[index] || 0); - }, 0); + const minDimension = prioritizeSmallerDimension(this.dimensions, args); + let sum = 0; + for (let i = 0; i < minDimension; i++) { + sum += this.values[i] * vals[i]; + } + return sum; } /** From 6fe93d7c94f016ac5894d166e63c6198d61efe91 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 19 May 2026 10:15:22 -0400 Subject: [PATCH 228/250] Attempt always checking infinite/non-number values --- src/math/p5.Vector.js | 18 +++++++++++------- src/math/patch-vector.js | 14 ++++++++------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 4f295bc1f2..497386d21a 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -78,18 +78,22 @@ class Vector { } this.values = args; - if (!Vector.friendlyErrorsDisabled() && Array.isArray(args)) { + if (Array.isArray(args)) { for (let i = 0; i < args.length; i++) { const v = args[i]; if (typeof v !== 'number' || !Number.isFinite(v)) { - this._friendlyError( - 'Arguments contain non-finite numbers', - 'p5.Vector' - ); + if (!Vector.friendlyErrorsDisabled()) { + this._friendlyError( + 'Arguments contain non-finite numbers', + 'p5.Vector' + ); + } + this.values = []; + break; } - this.values = []; - break; } + } else { + this.values = []; } // This property is here where duck typing (checking if obj.isVector) needs diff --git a/src/math/patch-vector.js b/src/math/patch-vector.js index 1aa65d0705..621a79c813 100644 --- a/src/math/patch-vector.js +++ b/src/math/patch-vector.js @@ -43,16 +43,18 @@ export function _validatedVectorOperation(expectsSoloNumberArgument){ args = new Array(3).fill(args[0]); } - if (!Vector.friendlyErrorsDisabled() && Array.isArray(args)) { + if (Array.isArray(args)) { for (let i = 0; i < args.length; i++) { const v = args[i]; if (typeof v !== 'number' || !Number.isFinite(v)) { - this._friendlyError( - 'Arguments contain non-finite numbers', - 'p5.Vector' - ); + if (!Vector.friendlyErrorsDisabled()) { + this._friendlyError( + 'Arguments contain non-finite numbers', + 'p5.Vector' + ); + return this; + } } - return this; } } From a29ab484e1569f23fb133a46c9639f7fadb0301c Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 19 May 2026 10:16:08 -0400 Subject: [PATCH 229/250] Also for div() --- src/math/p5.Vector.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 497386d21a..f188d1e3f3 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -1167,15 +1167,15 @@ class Vector { const minDimension = prioritizeSmallerDimension(this.dimensions, args); shrinkToDimension(this.values, minDimension); - if (!this.friendlyErrorsDisabled()) { - for (let i = 0; i < this.values.length; i++) { - if (typeof args[i] !== 'number' || args[i] === 0) { + for (let i = 0; i < this.values.length; i++) { + if (typeof args[i] !== 'number' || args[i] === 0) { + if (!this.friendlyErrorsDisabled()) { console.warn( 'p5.Vector.prototype.div', 'Arguments contain components that are 0' ); - return this; } + return this; } } From 0993f932346c9d95bdaba0755e215397fc3a1ef9 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 19 May 2026 10:22:39 -0400 Subject: [PATCH 230/250] Update location of binding cache --- src/math/math.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/math/math.js b/src/math/math.js index 2e4184962c..71a6848e87 100644 --- a/src/math/math.js +++ b/src/math/math.js @@ -92,11 +92,11 @@ function math(p5, fn) { * } */ fn.createVector = function (...args) { - if (!this._boundFromRadians) { - this._boundFromRadians = this._fromRadians.bind(this); - this._boundToRadians = this._toRadians.bind(this); - } if (this instanceof p5) { + if (!this._boundFromRadians) { + this._boundFromRadians = this._fromRadians.bind(this); + this._boundToRadians = this._toRadians.bind(this); + } return new p5.Vector(this._boundFromRadians, this._boundToRadians, ...args); } else { return new p5.Vector(...args); From 4a6945b07e0c9963f0f283ee786b7adca1f360d6 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 19 May 2026 10:38:58 -0400 Subject: [PATCH 231/250] Update how rem works to match --- src/math/p5.Vector.js | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index f188d1e3f3..1c912d4c93 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -643,7 +643,7 @@ class Vector { * let v2 = createVector(2, 3, 4); * * // Divide without modifying the original vectors. - * let v3 = p5.Vector.rem(v1, v2); + * let v3 = p5.Vector.rem(v1, v2); * * // Prints 'p5.Vector Object : [1, 1, 1]'. * print(v3.toString()); @@ -655,10 +655,24 @@ class Vector { */ rem(...args) { const minDimension = prioritizeSmallerDimension(this.dimensions, args); - shrinkToDimension(this.values, minDimension); + for (let i = 0; i < minDimension; i++) { + if (typeof args[i] !== 'number' || args[i] === 0) { + if (!this.friendlyErrorsDisabled()) { + console.warn( + 'p5.Vector.prototype.div', + 'Arguments contain components that are 0' + ); + } + return this; + } + } + + shrinkToDimension(this.values, minDimension); for (let i = 0; i < this.values.length; i++) { - this.values[i] = this.values[i] % args[i]; + if (args[i] > 0) { + this.values[i] = this.values[i] % args[i]; + } } return this; @@ -1165,9 +1179,8 @@ class Vector { */ div(...args) { const minDimension = prioritizeSmallerDimension(this.dimensions, args); - shrinkToDimension(this.values, minDimension); - for (let i = 0; i < this.values.length; i++) { + for (let i = 0; i < minDimension; i++) { if (typeof args[i] !== 'number' || args[i] === 0) { if (!this.friendlyErrorsDisabled()) { console.warn( @@ -1179,6 +1192,7 @@ class Vector { } } + shrinkToDimension(this.values, minDimension); for (let i = 0; i < this.values.length; i++) { this.values[i] *= args[i]; } From 606771a9f06948ca8e6bf80f1564fb638ad92ce8 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 19 May 2026 10:42:25 -0400 Subject: [PATCH 232/250] dot() bug --- src/math/p5.Vector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 1c912d4c93..08cc101b60 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -1378,7 +1378,7 @@ class Vector { if (args[0] instanceof Vector) { vals = args[0].values; } - const minDimension = prioritizeSmallerDimension(this.dimensions, args); + const minDimension = prioritizeSmallerDimension(this.dimensions, vals); let sum = 0; for (let i = 0; i < minDimension; i++) { sum += this.values[i] * vals[i]; From 2f674197a2a5cb76950124b32a5919a0eac11952 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 19 May 2026 10:50:44 -0400 Subject: [PATCH 233/250] Fix div --- src/math/p5.Vector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 08cc101b60..2f758ea9c3 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -1194,7 +1194,7 @@ class Vector { shrinkToDimension(this.values, minDimension); for (let i = 0; i < this.values.length; i++) { - this.values[i] *= args[i]; + this.values[i] /= args[i]; } return this; From f46887f4063dde471d2f84ec481388754ab5b645 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 19 May 2026 10:56:20 -0400 Subject: [PATCH 234/250] Move early return out of friendly errors check --- src/math/patch-vector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/math/patch-vector.js b/src/math/patch-vector.js index 621a79c813..7b4aab4b5a 100644 --- a/src/math/patch-vector.js +++ b/src/math/patch-vector.js @@ -52,8 +52,8 @@ export function _validatedVectorOperation(expectsSoloNumberArgument){ 'Arguments contain non-finite numbers', 'p5.Vector' ); - return this; } + return this; } } } From 434d1c238f1e1a7f33909f9b15420490d3d025ce Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 19 May 2026 11:50:11 -0400 Subject: [PATCH 235/250] Handle rem() like the old code --- src/math/p5.Vector.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 2f758ea9c3..33eddd1952 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -656,18 +656,6 @@ class Vector { rem(...args) { const minDimension = prioritizeSmallerDimension(this.dimensions, args); - for (let i = 0; i < minDimension; i++) { - if (typeof args[i] !== 'number' || args[i] === 0) { - if (!this.friendlyErrorsDisabled()) { - console.warn( - 'p5.Vector.prototype.div', - 'Arguments contain components that are 0' - ); - } - return this; - } - } - shrinkToDimension(this.values, minDimension); for (let i = 0; i < this.values.length; i++) { if (args[i] > 0) { From 83bb7e740eb0caeb99230cd998fb6b4356b09cd7 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 20 May 2026 14:35:37 -0400 Subject: [PATCH 236/250] Handle null tint in Renderer3D --- src/core/p5.Renderer3D.js | 7 +++++-- test/unit/visual/cases/webgl.js | 9 +++++++++ .../000.png | Bin 0 -> 7309 bytes .../metadata.json | 3 +++ 4 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGL/Opacity/noTint() before image() does not throw/000.png create mode 100644 test/unit/visual/screenshots/WebGL/Opacity/noTint() before image() does not throw/metadata.json diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 6974561b4f..d83df0170d 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -137,7 +137,7 @@ export class Renderer3D extends Renderer { this.states._useShininess = 1; this.states._useMetalness = 0; - this.states.tint = new Color([1, 1, 1, 1]); + this.states.tint = null; this.states.constantAttenuation = 1; this.states.linearAttenuation = 0; @@ -1502,7 +1502,10 @@ export class Renderer3D extends Renderer { // works differently and is global p5 state. If the p5 state has // been cleared, we also need to clear the value in uSampler to match. fillShader.setUniform("uSampler", this.states._tex || empty); - fillShader.setUniform("uTint", this.states.tint._getRGBA([255, 255, 255, 255])); + fillShader.setUniform( + "uTint", + this.states.tint?._getRGBA([255, 255, 255, 255]) ?? [255, 255, 255, 255] + ); fillShader.setUniform("uHasSetAmbient", this.states._hasSetAmbient); fillShader.setUniform("uAmbientMatColor", this.states.curAmbientColor); diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index fd0cb09d0a..7519d4e2ea 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -645,6 +645,15 @@ visualSuite('WebGL', function() { p5.circle(0, 0, 50); screenshot(); }); + + visualTest('noTint() before image() does not throw', async (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const img = await p5.loadImage('/test/unit/assets/cat.jpg'); + p5.noTint(); + p5.imageMode(p5.CENTER); + p5.image(img, 0, 0, 50, 50); + screenshot(); + }); }); visualSuite('Hooks coordinate spaces', () => { diff --git a/test/unit/visual/screenshots/WebGL/Opacity/noTint() before image() does not throw/000.png b/test/unit/visual/screenshots/WebGL/Opacity/noTint() before image() does not throw/000.png new file mode 100644 index 0000000000000000000000000000000000000000..c92e8ba0a89d4e782a9055b7ac318c424e4bfc2b GIT binary patch literal 7309 zcmV;89CG7{P)_ZK2Q{S z`mVD+uMe+RE;5G@I#YLg9!__kd9JGaueu-io-dWtr)t-(wf^<5f33YsJ#*`3Pmb^1 zah~_wo#%Vc-hQrk-(~B)uU~(v_sv_*^1glBIlR8rdwRzf@B2G1@Q>$jKi_-o>XZEI zPu_I8C&wdKZuFl1r>)+@SDfHIxa~OaiJQ*wzJ9|d?@OOv>pgPSiT-@wxcN-)o=f`t z-FDt;9&5cjE;_=y=hF4wLsuN{-Syc% z@1f5h=Y8$kQ@tl{IQ_qmZ+&r#_v~%wd8%BlVQOjy`}Q5c`1mBo#>Nl`29eL_QLR?t zI1WtHL{n1}T-SwR7?_=%MKl&gGMR+udC28*7#$t;zq8q_e{ZQ&^6%~H>Oxa`}ZzDZ34!JJ#%6%Ojorpvt$Ye5*kJuo#*tU&8Ab@Bz zibkX1+o?Pk2-ra+6HWfN3=re$^*W}fr!hP{>>G6O;6cpH%=qWzVeo)rvFIBp4lY`> z2(?-b%a$)kXJ;qGA$fSAP{7>WobS8`hNocY0pIAJo*r11<=asmaFCR7cr-97UV2-+wyQfWq0S32r+ zT%w-MLp zU~8J@(>G5*CGrBU%49M=qQY~X69jDAM?V&e348cZ59xH;cS@+Cl=6QI1tqjXBMG~l zm)sG~Tc+hB*dTu=CntR#$K!FxwLJ5wP^#nN?Kfg%I*VvBg5ftRQgm?b@ z`KZ%ad7wBcyq5LS9Kyd?JPMbgPtQ#I5fRS? zX+c2}OVF2dH7ZVy3YB3Z8;qoBSb(QdN#aN~#W6a35ItROm`hJ!HkW}ibwneirdL5r z`+Stx0RpyZnh@Jng-I+Y z4RT%6G@n+ItYUD4QYnat=Mr*3JdsGCnQ|%*$djXz#4@>{E)STaG<#4mi~zE^A}lJR z&G5H1$B|04V#VQon4FnIM^8JV(J@|^I>?|;CJ+O~%TH+5!mx^wf{Lh=OfF~$LW5K^87t9}oREhP3=LtB z=z`!BDpf2X1w#fhBzSirARZ^Z6M4FoG+389o;zUxQ$qOu;gO28MeWsH&pmy?nU zLLV6<{Fj?eWusiEVdT&R97ka)lSPGfVRm*J8VzY-5uKUIqF8WIgO05iT!h|5tT|Qy zWfr~h$q7uco-FEJhNjkbG{+M#8#T;M4k0@;f##Tnc^$2o-@TB++D4IFXwDOkRGJ(jLmk8>}) zf<0C{f>wf@G%%gZ0D&+%mM%lj!X=oP$-oT9P_8vFGB(PjPGcsQ!DP0GG+Q3nCIXfY zQ#oj93PQP6%uJ4yu5)~P*f}sE$ zQlnZevsDSh4p=l&qcL@8W(4~fjbN%1H6w`reIqc!NvvCc0xDVD)$Wso`{o=>NzNt2?VBa0WW zVAmT&ES4lsLrBu-BqxnFHDh#Q3TK_R2@z9AHIqg?J5B24(bW-0S62c{mM%bZD#<3Y z;wSWcwk`?EWSwBOceG)a^`}&=U@n*8GYpQaBA?Is(%atM&U~s<1hW1$p_ia8Y!RmT z=qm*|FUdZ`u0!aiOdjd!88(Y`6smRfE@2K(z5e}; zw^{8gFyX;qVbf8^*vJ5~bCXc2f%)@$(9_e2cp`#<{reCJ1xYIl(nL0=+R)M2gAi#b z51~_*G8;&^==`81JRDrq+t**f(J8EP^Fc|hb^Kd8vrRj9cKq8)m zG7Q6ih=fnqO_-+bhqGQU!!!bZzSO8fc_yjNaF*Gnim{vUzN7 zZpR6yY(csbL0Jz{+8vlwDe&X5HwIark8SUe0( zQD3B-GQfH`V)Ti z+W&*HEjaZCggGTHp`~R3KjPff)D#6J{a+E0{IH3(&gcY!I${j<(C~*?zG@*XLx-VP zFg`Sn(ZK=V$lv_-SD0h#yG<`26WFo!tEnJih2g5AV z@HzC(Z$n3C6uJVE@hCF0GvsU)A$9@t7#+{4qot*rW9GqW6cKO>KsAR{TPF@b;dJbo z$m92K4&t@H4C2Q7|2KAi?Ik?%+-oSg0eD6gJ-r?H-EV(Q+dLls^Uw2i7SB9vDx2pz zWQW7~8_Z452V4?goSNb8jtYlEkaa@2RAcsASg~@wuSSZEU;l@D5RJ^?{Wt!E!H?d; z!p<0)A`U`PfR~{g4$74R;_({CSCg-(WuD~h?GaqP+^OHgA+pam2ivd|!C z75=WV2t((&U;w&lK;?3I_A~9MYZ`fLqgXAXz&r>=lPDP}{Q2zzm}$h&v*tL2I=T_# z9+Soi7O@hyc6DO`D}5jw#Yi@Qo9}%N7hd-;Zh!1${Pyn$@y@PM1dQ;1Qw98Z&;UmE zqzx-@`jLl0(-br&wo@|z*MLx(!=Te4U8|sP?O~8;R46E-uxYj=LVcUGlHD39<}y~* zs}74;oeFF^4oVL6iXn?%D8ax z62ua51gSL5luVNoUw-y6xa$0qaQWtAargBXD>gX=K@YU^j zeCIYqxiMjA9_P>z(tsa+>p{$M^CHpOjzA;~!;Da|;#}M{5T+bUah{u##-YUhWRK7qG#YT|Kn?VS{}i-_3)Ltg!De!f4Q5kIg9+Zy000a&Nkl*AR!i2kisLKD)`A}bC~2-sbWQT*vh0? zT`SZ5Sk)DT%>+#`q8!GKphg4(&@|$}TrimBtL8Hw=cbTmW%sv(bPFx>79-J;qKIhd zX$H~*@8W~E{*5h8KlbkX2gb&BWA~oFVej5O$mKFnI_D#@nx?VCi1;m*3PeH>LoMB4 z+b~Xrs6zN0X0cngY#H148e-J$95)pt!6f0P*e_MMxEUK6!X$U^-+k*%R6Gv>y^dqo zEriA>D5{QO82&>vm!1$NlV6uzunBwKc>@Rf4`Fzyh_~L_gG1w0*exp&p1%fW%R+2C z2(HEjdYc!Uy~ zLzx!}+&O8`q{;LQG6g0qVHg@5LYZ4S4$tRVg%1r3;R8l3&Ip*%4gxZbYp*;9E0?xG zqUCaY{SYEH{s%&cHpclu0j9!EZ*F;7Y*0Moh}l3bH-(Y?d+;Bx{}F%um)9^$fI)?f#hQ`H6rp69EAJn~_%d7c z0IR4_OL8N`mL?pTPEp)Y% z@q_Q&fmdGm3V!$UQ#kd=r8IN|{exq$0#P({8+{v3!TDEg=Y2Nbc>7Jh?0F9d_V%N} zcHeUxcH*;8q^QyrDrAVRYp_idEvaVchRb|m=d5Z71U0B4+u~VDm}yyHo?>z;jS8a_ zWwquUNO9llqdj|IC7$$z+)1C1kh(_u;OOxcxtYGS}lWSX8RJ zn<(!22u0S2U?PSL6)eEQIhUD5tzJWvjik`A#(bzTr%Wq^nxhdAh1RY%L=#EuC%P#X zHr)wQR% z0}x=tS4E{1laQ%?wntqha-y%1&W&J)fKIuC+M%fOq zU^ZOxnQVq#dY)VRLDZc9 zJj;Nm*$8)b!K3kIR&Licz`X)YP_iSz5c0WMq#424*$gUk<5)UuP7Op-MtByYxV+9@iS$MX7{$`wf$&FaCaVxw)JFeOBRh)dzb{w3@qIJnyY`Oe; zAecnM45CnRal)AwV#8_YF@aa&#Lb_Andri%3%B9;%@^T_leS{(rPt%MPhXAZ#eF#L z>`QUsWmlqm*-<#<{L5jr^305C}27=KrvP|YR$DWFL z%h%$VlTXIs$DNKNPS}jK$8H4M59kylrixE%HOBPs&m9_Cn0Ls4o4Jm{u{A|-a&@@1SuN61vT`n*$MI8`XE zhFdP)f=WK)(@P>ExvNNjn6U(NA_@fqN>*OHUFpEV!9$RWyZwf{u%hp5gjyCrr=04p zjwMGOjiyKfpksUvh7Rt-Zf-=ll_E4{z_LPcJdKK$3S}q_nqk2*U1$W&^Au`c z0h(4KMH=}2k6z_iK3H>*WS8*KuJ>T*hTsBAcj1w4n$pnO8bN|>QHTI^&Fe<8sTs;5 z`XYr>eD7Uv)WBK&L!+d1HTS~sfIs}-pTSaPz%z+jm6&;GZR-XVmd1K4ip^;2oaev6 zJ#=6{T|RL`z#I4e>C<*KsbxB?&4jC~^(Xr1)ZIiVHVg*Kz&lx67??DB*Iw zjj!0`gwFd6URRn4uxgiz74(0+7ybSH?9E0{VI-COCrJ|w1)*p>|Kq(L9|35LkVaZa zKPC>#%O1nBq3J@w5TnJ?NtBvW9dx7{UET9A$km|-MI?0$?(b*SPmsOm7;y;UHtE@!?8X9*r9Y^j4?{Oh7pXdWTju#N1DS#15r{@p|2C-nl0w}SWGpk5A z2FrG#SShKcaJ?A7w^Q&qQ9X|aU%s64sVi{QsoOb^LLobeT4fe>H-iY9O9NG0d*^+a zEf#R-&_S-=a&Y)+4ctGD1wFW`&fhfzG>=Vooe?czk}q`}F+f)^0}j}260#x^ts%Je z0t+NCna-f5g<*Pmc$_cDhyXQ!e=Dk0gGIASNlr=)42PhW^e%$g(9xL7;fecq;PHF5 z@^8hkI4RW5+WPV>4Ky1ct&kw~TU}AWzEJ zh=x;CtT}{&4gz)wO(_r2mKe+?MmMU^lGIRja+u7F!Ejg&Jul&NJHLbNcfEic9{Mr9@W^Y}@yJhc!#&@}&aeLq?s($2xbfZ} zVcRW_VB4*aW7`)W#pO5Nk4vw;6PI4|C0uglZMf{3dvV+S-@}W)`VcSw+Y}!A Date: Wed, 20 May 2026 14:43:21 -0400 Subject: [PATCH 237/250] Update WebGL tests to allow null tint --- test/unit/webgl/p5.RendererGL.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 93582483b6..079b8000f6 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1517,10 +1517,12 @@ suite('p5.RendererGL', function() { }); suite('tint() in WEBGL mode', function() { - test('default tint value is set and not null', function() { + test('default tint value', function() { myp5.createCanvas(100, 100, myp5.WEBGL); - assert.deepEqual(myp5._renderer.states.tint - ._getRGBA([255, 255, 255, 255]), [255, 255, 255, 255]); + assert.deepEqual( + myp5._renderer.states.tint?._getRGBA([255, 255, 255, 255]) ?? [255, 255, 255, 255], + [255, 255, 255, 255] + ); }); @@ -1581,7 +1583,7 @@ suite('p5.RendererGL', function() { }; }); }).then(function(_tint) { - assert.deepEqual(_tint._getRGBA([255, 255, 255, 255]), + assert.deepEqual(_tint?._getRGBA([255, 255, 255, 255]) ?? [255, 255, 255, 255], [255, 255, 255, 255]); }); }); From 5c1872306d91490ffe5538978bb52346cf608a2a Mon Sep 17 00:00:00 2001 From: perminder Date: Thu, 21 May 2026 07:06:09 +0530 Subject: [PATCH 238/250] fixing-noise-reference-for-strands --- src/math/noise.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/math/noise.js b/src/math/noise.js index 2752115494..878b8f739f 100644 --- a/src/math/noise.js +++ b/src/math/noise.js @@ -71,7 +71,7 @@ function noise(p5, fn){ * @param {Number} [z] z-coordinate in noise space. * @return {Number} Perlin noise value at specified coordinates. * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -89,8 +89,9 @@ function noise(p5, fn){ * strokeWeight(5); * point(x, y); * } + * ``` * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -115,8 +116,9 @@ function noise(p5, fn){ * strokeWeight(5); * point(x, y); * } + * ``` * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -138,8 +140,9 @@ function noise(p5, fn){ * // Draw the line. * line(x, 0, x, y); * } + * ``` * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -166,8 +169,9 @@ function noise(p5, fn){ * line(x, 0, x, y); * } * } + * ``` * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -196,8 +200,9 @@ function noise(p5, fn){ * * describe('A gray cloudy pattern.'); * } + * ``` * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -227,6 +232,7 @@ function noise(p5, fn){ * } * } * } + * ``` * * `noise()` can also be used in shaders with p5.strands, where it returns * values in the range 0 to 1. The following example uses `noise()` to create @@ -501,4 +507,4 @@ export default noise; if(typeof p5 !== 'undefined'){ noise(p5, p5.prototype); -} +} \ No newline at end of file From d9862e262fe69bdf301764e2d2a4cb7f22e7710b Mon Sep 17 00:00:00 2001 From: perminder Date: Thu, 21 May 2026 07:19:34 +0530 Subject: [PATCH 239/250] fixes --- src/math/noise.js | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/math/noise.js b/src/math/noise.js index 878b8f739f..c621633439 100644 --- a/src/math/noise.js +++ b/src/math/noise.js @@ -71,7 +71,11 @@ function noise(p5, fn){ * @param {Number} [z] z-coordinate in noise space. * @return {Number} Perlin noise value at specified coordinates. * - * ```js example + * `noise()` can also be used in shaders with p5.strands, where it returns + * values in the range 0 to 1. One of the examples below uses `noise()` to + * create a cloud-like texture effect in a filter shader. + * + * @example * function setup() { * createCanvas(100, 100); * @@ -89,9 +93,8 @@ function noise(p5, fn){ * strokeWeight(5); * point(x, y); * } - * ``` * - * ```js example + * @example * function setup() { * createCanvas(100, 100); * @@ -116,9 +119,8 @@ function noise(p5, fn){ * strokeWeight(5); * point(x, y); * } - * ``` * - * ```js example + * @example * function setup() { * createCanvas(100, 100); * @@ -140,9 +142,8 @@ function noise(p5, fn){ * // Draw the line. * line(x, 0, x, y); * } - * ``` * - * ```js example + * @example * function setup() { * createCanvas(100, 100); * @@ -169,9 +170,8 @@ function noise(p5, fn){ * line(x, 0, x, y); * } * } - * ``` * - * ```js example + * @example * function setup() { * createCanvas(100, 100); * @@ -200,9 +200,8 @@ function noise(p5, fn){ * * describe('A gray cloudy pattern.'); * } - * ``` * - * ```js example + * @example * function setup() { * createCanvas(100, 100); * @@ -232,13 +231,8 @@ function noise(p5, fn){ * } * } * } - * ``` * - * `noise()` can also be used in shaders with p5.strands, where it returns - * values in the range 0 to 1. The following example uses `noise()` to create - * a cloud-like texture effect in a filter shader. - * - * ```js example + * @example * let myFilter; * * function setup() { @@ -262,7 +256,6 @@ function noise(p5, fn){ * function draw() { * filter(myFilter); * } - * ``` */ fn.noise = function(x, y = 0, z = 0) { if (perlin == null) { From e7d4366bd5242a8ff5a04fd922b1b0d875ecc6db Mon Sep 17 00:00:00 2001 From: perminder Date: Thu, 21 May 2026 08:16:07 +0530 Subject: [PATCH 240/250] fixing --- src/math/noise.js | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/math/noise.js b/src/math/noise.js index c621633439..fcc26a42d8 100644 --- a/src/math/noise.js +++ b/src/math/noise.js @@ -65,17 +65,7 @@ function noise(p5, fn){ * three dimensions. These dimensions can be thought of as space, as in * `noise(x, y, z)`, or space and time, as in `noise(x, y, t)`. * - * @method noise - * @param {Number} x x-coordinate in noise space. - * @param {Number} [y] y-coordinate in noise space. - * @param {Number} [z] z-coordinate in noise space. - * @return {Number} Perlin noise value at specified coordinates. - * - * `noise()` can also be used in shaders with p5.strands, where it returns - * values in the range 0 to 1. One of the examples below uses `noise()` to - * create a cloud-like texture effect in a filter shader. - * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -93,8 +83,9 @@ function noise(p5, fn){ * strokeWeight(5); * point(x, y); * } + * ``` * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -119,8 +110,9 @@ function noise(p5, fn){ * strokeWeight(5); * point(x, y); * } + * ``` * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -142,8 +134,9 @@ function noise(p5, fn){ * // Draw the line. * line(x, 0, x, y); * } + * ``` * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -170,8 +163,9 @@ function noise(p5, fn){ * line(x, 0, x, y); * } * } + * ``` * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -200,8 +194,9 @@ function noise(p5, fn){ * * describe('A gray cloudy pattern.'); * } + * ``` * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -231,8 +226,13 @@ function noise(p5, fn){ * } * } * } + * ``` * - * @example + * `noise()` can also be used in shaders with p5.strands, where it returns + * values in the range 0 to 1. The example below uses `noise()` inside a + * filter shader to create a cloud-like texture effect: + * + * ```js example * let myFilter; * * function setup() { @@ -256,6 +256,13 @@ function noise(p5, fn){ * function draw() { * filter(myFilter); * } + * ``` + * + * @method noise + * @param {Number} x x-coordinate in noise space. + * @param {Number} [y] y-coordinate in noise space. + * @param {Number} [z] z-coordinate in noise space. + * @return {Number} Perlin noise value at specified coordinates. */ fn.noise = function(x, y = 0, z = 0) { if (perlin == null) { From a33d206bccfa70839a640f7e5e5de01f8146ca3c Mon Sep 17 00:00:00 2001 From: perminder Date: Thu, 21 May 2026 08:23:44 +0530 Subject: [PATCH 241/250] fixing-random-and-millis --- src/math/random.js | 28 +++++++++++++++++----------- src/utilities/time_date.js | 17 +++++++++++------ 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/math/random.js b/src/math/random.js index f6bc21eb3d..717c6bccc1 100644 --- a/src/math/random.js +++ b/src/math/random.js @@ -105,12 +105,7 @@ function random(p5, fn){ * For example, calling `random(-5, 10.2)` returns values from -5 up to but * not including 10.2. * - * @method random - * @param {Number} [min] lower bound (inclusive). - * @param {Number} [max] upper bound (exclusive). - * @return {Number} random number. - * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -126,8 +121,9 @@ function random(p5, fn){ * * describe('A black dot appears in a random position on a gray square.'); * } + * ``` * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -143,8 +139,9 @@ function random(p5, fn){ * * describe('A black dot appears in a random position on a gray square.'); * } + * ``` * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -165,8 +162,9 @@ function random(p5, fn){ * * describe('An animal face is displayed at random. Either a lion, tiger, or bear.'); * } + * ``` * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -187,8 +185,9 @@ function random(p5, fn){ * strokeWeight(5); * point(x, y); * } + * ``` * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -209,8 +208,9 @@ function random(p5, fn){ * strokeWeight(5); * point(x, y); * } + * ``` * - * @example + * ```js example * let x = 50; * let y = 50; * @@ -230,6 +230,7 @@ function random(p5, fn){ * // Draw the point. * point(x, y); * } + * ``` * * `random()` can also be used in shaders with p5.strands. The following example * uses `random()` to create varying colors on a shape. @@ -259,6 +260,11 @@ function random(p5, fn){ * sphere(30); * } * ``` + * + * @method random + * @param {Number} [min] lower bound (inclusive). + * @param {Number} [max] upper bound (exclusive). + * @return {Number} random number. */ /** * @method random diff --git a/src/utilities/time_date.js b/src/utilities/time_date.js index 5b85c4dcbb..5b0b4c154b 100644 --- a/src/utilities/time_date.js +++ b/src/utilities/time_date.js @@ -108,10 +108,8 @@ function timeDate(p5, fn){ * sketch includes asynchronous loading using `async`/`await`, then * `millis()` begins tracking time as soon as the asynchronous code * starts running. - * @method millis - * @return {Number} number of milliseconds since starting the sketch. * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -132,8 +130,9 @@ function timeDate(p5, fn){ * `The text 'Startup time: ${round(ms, 2)} ms' written in black on a gray background.` * ); * } + * ``` * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -154,8 +153,9 @@ function timeDate(p5, fn){ * // Display how long the sketch has run. * text(`Running time: ${nf(s, 1, 1)} sec`, 5, 50, 90); * } + * ``` * - * @example + * ```js example * function setup() { * createCanvas(100, 100); * @@ -174,8 +174,9 @@ function timeDate(p5, fn){ * // Draw the circle. * circle(x, 50, 30); * } + * ``` * - * @example + * ```js example * async function setup() { * // Load the GeoJSON. * await loadJSON('https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson'); @@ -198,6 +199,7 @@ function timeDate(p5, fn){ * `The text "It took ${round(ms, 2)} ms to load the data" written in black on a gray background.` * ); * } + * ``` * * `millis()` can also be used in shaders with p5.strands. The following example * uses `millis()` to create time-based color transitions on a shape. @@ -228,6 +230,9 @@ function timeDate(p5, fn){ * sphere(30); * } * ``` + * + * @method millis + * @return {Number} number of milliseconds since starting the sketch. */ fn.millis = function() { if (this._millisStart === -1) { From e34e5abfb71b153e64bfc4ed9038458f057f9f06 Mon Sep 17 00:00:00 2001 From: kit Date: Thu, 21 May 2026 22:58:05 +0200 Subject: [PATCH 242/250] 2.3.0-rc.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index fcfeaeb678..bb9afb981a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "p5", - "version": "2.3.0-rc.4", + "version": "2.3.0-rc.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "p5", - "version": "2.3.0-rc.4", + "version": "2.3.0-rc.5", "license": "LGPL-2.1", "dependencies": { "@davepagurek/bezier-path": "^0.0.7", diff --git a/package.json b/package.json index 5993cbfb3b..92abc303a8 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test/**/*.js": "eslint", "utils/**/*.{js,mjs}": "eslint" }, - "version": "2.3.0-rc.4", + "version": "2.3.0-rc.5", "dependencies": { "@davepagurek/bezier-path": "^0.0.7", "@japont/unicode-range": "^1.0.0", From 4e08d4a3edd9ee1187570cbc6586b944b16812a6 Mon Sep 17 00:00:00 2001 From: nbogie Date: Fri, 22 May 2026 19:43:53 +0100 Subject: [PATCH 243/250] use snake not kebab case in example path --- contributor_docs/working_with_contributor_documents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributor_docs/working_with_contributor_documents.md b/contributor_docs/working_with_contributor_documents.md index fbe58aa992..ef45617347 100644 --- a/contributor_docs/working_with_contributor_documents.md +++ b/contributor_docs/working_with_contributor_documents.md @@ -154,7 +154,7 @@ As a result, this will list a page titled "Unit Testing" with a description of " #### The URL for your document The path in the eventual URL will be -`contribute/your-filename-without-extension/` +`contribute/your_filename_without_extension/` Note that the trailing slash is necessary in development mode. Example: From c0aed016567c5954c24092c3b66971b87dee30b6 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Fri, 22 May 2026 19:45:54 +0100 Subject: [PATCH 244/250] Vector mismatch dimension prints warning message --- src/math/p5.Vector.js | 78 ++++++++++++++++++++++++++++------------ src/math/patch-vector.js | 22 +++++++++--- 2 files changed, 73 insertions(+), 27 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 33eddd1952..f2635ceaba 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -9,8 +9,14 @@ import * as constants from '../core/constants'; * This function is used by binary vector operations to prioritize shorter vectors, * and to emit a warning when lengths do not match. */ -const prioritizeSmallerDimension = function(currentVectorDimension, args) { - return Math.min(currentVectorDimension, args.length); +const prioritizeSmallerDimension = function (currentVectorDimension, args) { + const resultDimension = Math.min(currentVectorDimension, args.length); + if (Array.isArray(args) && currentVectorDimension !== args.length) { + console.warn( + 'When working with two vectors of different sizes, the smaller dimension is used. In this operation, both vector will be treated as ' + resultDimension + 'D vectors, and any additional values of the linger vector will be ignored.' + ); + } + return resultDimension; }; /** @@ -529,7 +535,7 @@ class Vector { * @param {p5.Vector|Number[]} value The vector to add * @chainable */ - add(...args) { + add(args) { const minDimension = prioritizeSmallerDimension(this.dimensions, args); shrinkToDimension(this.values, minDimension); @@ -653,14 +659,19 @@ class Vector { * @param {p5.Vector | Number[]} value divisor vector. * @chainable */ - rem(...args) { + rem(args) { const minDimension = prioritizeSmallerDimension(this.dimensions, args); shrinkToDimension(this.values, minDimension); - for (let i = 0; i < this.values.length; i++) { - if (args[i] > 0) { + + if(Array.isArray(args)){ + for (let i = 0; i < this.values.length; i++) { this.values[i] = this.values[i] % args[i]; } + } else { + for (let i = 0; i < this.values.length; i++) { + this.values[i] = this.values[i] % args; + } } return this; @@ -788,7 +799,7 @@ class Vector { * @param {p5.Vector|Number[]} value the vector to subtract * @chainable */ - sub(...args) { + sub(args) { const minDimension = prioritizeSmallerDimension(this.dimensions, args); shrinkToDimension(this.values, minDimension); @@ -976,12 +987,18 @@ class Vector { * @param {p5.Vector} v vector to multiply with the components of the original vector. * @chainable */ - mult(...args) { + mult(args) { const minDimension = prioritizeSmallerDimension(this.dimensions, args); shrinkToDimension(this.values, minDimension); - for (let i = 0; i < this.values.length; i++) { - this.values[i] *= args[i]; + if(Array.isArray(args)){ + for (let i = 0; i < this.values.length; i++) { + this.values[i] *= args[i]; + } + } else { + for (let i = 0; i < this.values.length; i++) { + this.values[i] *= args; + } } return this; @@ -1165,24 +1182,41 @@ class Vector { * @param {p5.Vector} v vector to divide the components of the original vector by. * @chainable */ - div(...args) { + div(args) { const minDimension = prioritizeSmallerDimension(this.dimensions, args); - for (let i = 0; i < minDimension; i++) { - if (typeof args[i] !== 'number' || args[i] === 0) { - if (!this.friendlyErrorsDisabled()) { - console.warn( - 'p5.Vector.prototype.div', - 'Arguments contain components that are 0' - ); + if (Array.isArray(args)) { + for (let i = 0; i < minDimension; i++) { + if ((typeof args[i] !== 'number' || args[i] === 0)) { + if (!this.friendlyErrorsDisabled()) { + console.warn( + 'p5.Vector.prototype.div', + 'Arguments contain components that are 0' + ); + } + return this; } - return this; } + } else if(typeof args !== 'number' || args === 0) { + if (!this.friendlyErrorsDisabled()) { + console.warn( + 'p5.Vector.prototype.div', + 'Arguments contain components that are 0' + ); + } + return this; } shrinkToDimension(this.values, minDimension); - for (let i = 0; i < this.values.length; i++) { - this.values[i] /= args[i]; + + if(Array.isArray(args)){ + for (let i = 0; i < this.values.length; i++) { + this.values[i] /= args[i]; + } + } else { + for (let i = 0; i < this.values.length; i++) { + this.values[i] /= args; + } } return this; @@ -1361,7 +1395,7 @@ class Vector { * @param {p5.Vector} v p5.Vector to be dotted. * @return {Number} */ - dot(...args) { + dot(args) { let vals = args; if (args[0] instanceof Vector) { vals = args[0].values; diff --git a/src/math/patch-vector.js b/src/math/patch-vector.js index 7b4aab4b5a..d7f683c299 100644 --- a/src/math/patch-vector.js +++ b/src/math/patch-vector.js @@ -28,7 +28,7 @@ export function _defaultEmptyVector(target){ */ export function _validatedVectorOperation(expectsSoloNumberArgument){ return function(target){ - return function(...args){ + return function (...args) { if (args.length === 0) { // No arguments? No action return this; @@ -38,12 +38,14 @@ export function _validatedVectorOperation(expectsSoloNumberArgument){ } else if (Array.isArray(args[0])) { // First argument is an array? Great, keep it! args = args[0]; - } else if (expectsSoloNumberArgument && args.length === 1){ + } else if (args.length === 1){ // Special case for a solo numeric arguments only applies sometimes - args = new Array(3).fill(args[0]); + if (expectsSoloNumberArgument) { + args = args[0]; + } } - if (Array.isArray(args)) { + if(Array.isArray(args)){ for (let i = 0; i < args.length; i++) { const v = args[i]; if (typeof v !== 'number' || !Number.isFinite(v)) { @@ -56,9 +58,19 @@ export function _validatedVectorOperation(expectsSoloNumberArgument){ return this; } } + } else { + if (typeof args !== 'number' || !Number.isFinite(args)) { + if (!Vector.friendlyErrorsDisabled()) { + this._friendlyError( + 'Arguments contain non-finite numbers', + 'p5.Vector' + ); + } + return this; + } } - return target.call(this, ...args); + return target.call(this, args); }; }; } From cb814cadb26804e295b62e035e817c1c03db4310 Mon Sep 17 00:00:00 2001 From: nbogie Date: Fri, 22 May 2026 19:54:01 +0100 Subject: [PATCH 245/250] Clarify in build overview that exact details will follow... ...and mention some limitations of quick preview. --- contributor_docs/working_with_contributor_documents.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contributor_docs/working_with_contributor_documents.md b/contributor_docs/working_with_contributor_documents.md index ef45617347..54072d3a72 100644 --- a/contributor_docs/working_with_contributor_documents.md +++ b/contributor_docs/working_with_contributor_documents.md @@ -21,7 +21,7 @@ Their source materials are kept in: ## Build process overview -During the website build process `build:contributor-docs`, the documents are cloned from the requested branch of the p5.js repo into the relevant website file-system locations. +During the _website_ build process `build:contributor-docs` the documents are cloned from the requested branch of the p5.js repo into the relevant website file-system locations. From here, the astro dev server can show previews of how they will look. (Exact instructions and paths follow.) ## Generating and previewing contributor documents @@ -32,7 +32,7 @@ For a quick preview, various editors have a feature to render markdown files. F * open the command-palette (`F1` or `cmd-shift-p` or `ctrl-shift-p`) * type `Markdown: open preview` - +There are various limitations to this quick-preview. For example, the p5.js website page layout and styling (colors, fonts, line-width, etc) will not be applied. ### Preview on local p5.js-website clone From 034a00f8304c18d08c29189cea9dd4cfffbcee1e Mon Sep 17 00:00:00 2001 From: nbogie Date: Fri, 22 May 2026 19:55:38 +0100 Subject: [PATCH 246/250] move TODOs out into GH issue conversation --- .../working_with_contributor_documents.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/contributor_docs/working_with_contributor_documents.md b/contributor_docs/working_with_contributor_documents.md index 54072d3a72..fd7d660bd7 100644 --- a/contributor_docs/working_with_contributor_documents.md +++ b/contributor_docs/working_with_contributor_documents.md @@ -161,15 +161,3 @@ Example: The source document `contributor_docs/unit_testing.md` will be served as `https://beta.p5js.org/contribute/unit_testing/` - - - From 42e4670573384945712b160837b2f4921a6535e0 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Fri, 22 May 2026 20:11:36 +0100 Subject: [PATCH 247/250] Fix remainder 0 handling --- src/math/p5.Vector.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index f2635ceaba..fcd70b1680 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -666,9 +666,11 @@ class Vector { if(Array.isArray(args)){ for (let i = 0; i < this.values.length; i++) { - this.values[i] = this.values[i] % args[i]; + if (args[i] > 0) { + this.values[i] = this.values[i] % args[i]; + } } - } else { + } else if(args > 0) { for (let i = 0; i < this.values.length; i++) { this.values[i] = this.values[i] % args; } From 7bdc6d014663fc30bfd33a07625e564234be73b9 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Fri, 22 May 2026 20:18:17 +0100 Subject: [PATCH 248/250] Fix dot product unintended change --- src/math/p5.Vector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index fcd70b1680..f5464700ca 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -1397,7 +1397,7 @@ class Vector { * @param {p5.Vector} v p5.Vector to be dotted. * @return {Number} */ - dot(args) { + dot(...args) { let vals = args; if (args[0] instanceof Vector) { vals = args[0].values; From dc3817670769c36e9ef39aa940e5f7dc2940f561 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sat, 23 May 2026 11:08:15 +0100 Subject: [PATCH 249/250] Update vector documentation for consistency --- src/math/p5.Vector.js | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index f5464700ca..4c1952a16b 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -405,8 +405,6 @@ class Vector { } } - - /** * Adds to a vector's components. * @@ -414,11 +412,10 @@ class Vector { * another p5.Vector object, as in `v.add(v2)`, or * an array of numbers, as in `v.add([1, 2, 3])`. * - * Add vectors only when they are the same size: both 2-dimensional, or - * both 3-dimensional. When two vectors of different sizes are added, the - * smaller dimension will be used, any additional values of the longer - * vector will be ignored. - * For example, adding `[1, 2, 3]` and `[4, 5]` will result in `[5, 7]`. + * You should add vectors only when they are the same size. When two vectors + * of different sizes are added, the smaller dimension will be used, any + * additional values of the longer vector will be ignored. For example, + * adding `[1, 2, 3]` and `[4, 5]` will result in `[5, 7]`. * * Calling `add()` with no arguments, as in `v.add()`, has no effect. * @@ -546,11 +543,8 @@ class Vector { return this; } - - /** - * Performs modulo (remainder) division with a vector's `x`, `y`, and `z` - * components. + * Performs modulo (remainder) division with a vector's components. * * `rem()` can use separate numbers, as in `v.rem(1, 2, 3)`, * another p5.Vector object, as in `v.rem(v2)`, or @@ -560,8 +554,8 @@ class Vector { * will be set to their values modulo 2. Calling `rem()` with no * arguments, as in `v.rem()`, has no effect. * - * Modulo vectors only when they are the same size: both 2D, or both 3D. - * When two vectors of different sizes are used, the smaller dimension will be + * You should modulo vectors only when they are the same size. When two + * vectors of different sizes are used, the smaller dimension will be * used, any additional values of the longer vector will be ignored. * For example, taking `[3, 6, 9]` modulo `[2, 4]` will result in `[1, 2]`. * @@ -680,7 +674,7 @@ class Vector { } /** - * Subtracts from a vector's `x`, `y`, and `z` components. + * Subtracts from a vector's components. * * `sub()` can use separate numbers, as in `v.sub(1, 2, 3)`, another * p5.Vector object, as in `v.sub(v2)`, or an array @@ -688,8 +682,8 @@ class Vector { * * Calling `sub()` with no arguments, as in `v.sub()`, has no effect. * - * Subtract vectors only when they are the same size: both 2D, or both 3D. - * When two vectors of different sizes are used, the smaller dimension will be + * You should subtract vectors only when they are the same size. When two + * vectors of different sizes are used, the smaller dimension will be * used, any additional values of the longer vector will be ignored. * For example, subtracting `[1, 2]` from `[3, 5, 7]` will result in `[2, 3]`. * @@ -813,7 +807,7 @@ class Vector { } /** - * Multiplies a vector's `x`, `y`, and `z` components. + * Multiplies a vector's components. * * `mult()` can use separate numbers, as in `v.mult(1, 2, 3)`, another * p5.Vector object, as in `v.mult(v2)`, or an array @@ -823,8 +817,8 @@ class Vector { * will be multiplied by 2. Calling `mult()` with no arguments, as in `v.mult()`, has * no effect. * - * Multiply vectors only when they are the same size: both 2D, or both 3D. - * When two vectors of different sizes are multiplied, the smaller dimension will be + * You should multiply vectors only when they are the same size. When two + * vectors of different sizes are multiplied, the smaller dimension will be * used, any additional values of the longer vector will be ignored. * For example, multiplying `[1, 2, 3]` by `[4, 5]` will result in `[4, 10]`. * @@ -1007,7 +1001,7 @@ class Vector { } /** - * Divides a vector's `x`, `y`, and `z` components. + * Divides a vector's components. * * `div()` can use separate numbers, as in `v.div(1, 2, 3)`, another * p5.Vector object, as in `v.div(v2)`, or an array @@ -1017,8 +1011,8 @@ class Vector { * will be divided by 2. Calling `div()` with no arguments, as in `v.div()`, has * no effect. * - * Divide vectors only when they are the same size: both 2D, or both 3D. - * When two vectors of different sizes are divided, the smaller dimension will be + * You should divide vectors only when they are the same size. When two + * vectors of different sizes are divided, the smaller dimension will be * used, any additional values of the longer vector will be ignored. * For example, dividing `[8, 12, 21]` by `[2, 3]` will result in `[4, 4]`. * From 030b18a169a3b106b48687d1b4881f0f04c51c3c Mon Sep 17 00:00:00 2001 From: kit Date: Tue, 26 May 2026 14:54:38 +0200 Subject: [PATCH 250/250] 2.3.0-rc.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bb9afb981a..5abf442b43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "p5", - "version": "2.3.0-rc.5", + "version": "2.3.0-rc.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "p5", - "version": "2.3.0-rc.5", + "version": "2.3.0-rc.6", "license": "LGPL-2.1", "dependencies": { "@davepagurek/bezier-path": "^0.0.7", diff --git a/package.json b/package.json index 92abc303a8..df19866fe3 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test/**/*.js": "eslint", "utils/**/*.{js,mjs}": "eslint" }, - "version": "2.3.0-rc.5", + "version": "2.3.0-rc.6", "dependencies": { "@davepagurek/bezier-path": "^0.0.7", "@japont/unicode-range": "^1.0.0",