From 0251a238f0c3bd6710c372b355b3f4d62eec1531 Mon Sep 17 00:00:00 2001 From: Avinash Kumar Deepak Date: Tue, 28 Oct 2025 00:37:26 +0530 Subject: [PATCH 01/11] feat(webgl): add global property support for p5.strands --- src/strands/strands_api.js | 67 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index f4bfabcea2..6b6a46cbac 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -168,6 +168,73 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } } } + ////////////////////////////////////////////// + // Global p5 properties + ////////////////////////////////////////////// + const globalProperties = [ + { name: 'width', type: DataType.float1 }, + { name: 'height', type: DataType.float1 }, + { name: 'mouseX', type: DataType.float1 }, + { name: 'mouseY', type: DataType.float1 }, + { name: 'pmouseX', type: DataType.float1 }, + { name: 'pmouseY', type: DataType.float1 }, + { name: 'winMouseX', type: DataType.float1 }, + { name: 'winMouseY', type: DataType.float1 }, + { name: 'frameCount', type: DataType.int1 }, + { name: 'focused', type: DataType.bool1 }, + { name: 'displayWidth', type: DataType.float1 }, + { name: 'displayHeight', type: DataType.float1 }, + { name: 'windowWidth', type: DataType.float1 }, + { name: 'windowHeight', type: DataType.float1 }, + { name: 'mouseButton', type: DataType.int1 }, + { name: 'mouseIsPressed', type: DataType.bool1 } + ]; + + const originalDescriptors = {}; + for (const { name } of globalProperties) { + originalDescriptors[name] = Object.getOwnPropertyDescriptor(fn, name) || { + get: function() { return p5.prototype[name]; }, + configurable: true + }; + } + + for (const { name, type } of globalProperties) { + strandsContext.fnOverrides[name] = originalDescriptors[name]; + + (function(propName, propType, origDescriptor) { + Object.defineProperty(fn, propName, { + get: function() { + if (strandsContext.active) { + const uniformName = `_p5_${propName}`; + const existingUniform = strandsContext.uniforms.find(u => u.name === uniformName); + + if (!existingUniform) { + const { id, dimension } = build.variableNode(strandsContext, propType, uniformName); + strandsContext.uniforms.push({ + name: uniformName, + typeInfo: propType, + defaultValue: origDescriptor.get ? + () => origDescriptor.get.call(fn) : + () => origDescriptor.value + }); + return createStrandsNode(id, dimension, strandsContext); + } else { + const { id, dimension } = build.variableNode(strandsContext, propType, uniformName); + return createStrandsNode(id, dimension, strandsContext); + } + } else { + if (origDescriptor.get) { + return origDescriptor.get.call(this); + } else { + return origDescriptor.value; + } + } + }, + configurable: true, + enumerable: true + }); + })(name, type, originalDescriptors[name]); + } } ////////////////////////////////////////////// // Per-Hook functions From 12a3f50459e80fe9e07841f3c17372a11a74c2de Mon Sep 17 00:00:00 2001 From: Avinash Kumar Deepak Date: Mon, 24 Nov 2025 14:43:14 +0530 Subject: [PATCH 02/11] fix: change frameCount to float1, remove mouseButton, add unit tests - Changed frameCount from int1 to float1 for JS compatibility - Removed mouseButton (object type in p5 2.x) - Added unit tests for all 14 global properties Addresses feedback from @davepagurek in #8211 --- src/strands/strands_api.js | 3 +- test/unit/webgl/strands.js | 143 +++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 test/unit/webgl/strands.js diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 6b6a46cbac..cd444998ae 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -180,13 +180,12 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { { name: 'pmouseY', type: DataType.float1 }, { name: 'winMouseX', type: DataType.float1 }, { name: 'winMouseY', type: DataType.float1 }, - { name: 'frameCount', type: DataType.int1 }, + { name: 'frameCount', type: DataType.float1 }, { name: 'focused', type: DataType.bool1 }, { name: 'displayWidth', type: DataType.float1 }, { name: 'displayHeight', type: DataType.float1 }, { name: 'windowWidth', type: DataType.float1 }, { name: 'windowHeight', type: DataType.float1 }, - { name: 'mouseButton', type: DataType.int1 }, { name: 'mouseIsPressed', type: DataType.bool1 } ]; diff --git a/test/unit/webgl/strands.js b/test/unit/webgl/strands.js new file mode 100644 index 0000000000..0cff454779 --- /dev/null +++ b/test/unit/webgl/strands.js @@ -0,0 +1,143 @@ +suite('p5.strands global properties', function() { + test('width and height are defined', function() { + const p = new p5((sketch) => { + sketch.setup = function() { + sketch.createCanvas(200, 150); + }; + sketch.draw = function() {}; + }); + + assert.equal(p.width, 200); + assert.equal(p.height, 150); + p.remove(); + }); + + test('mouseX and mouseY are numbers', function() { + const p = new p5((sketch) => { + sketch.setup = function() { + sketch.createCanvas(100, 100); + }; + sketch.draw = function() {}; + }); + + assert.isNumber(p.mouseX); + assert.isNumber(p.mouseY); + p.remove(); + }); + + test('frameCount is a number', function() { + const p = new p5((sketch) => { + sketch.setup = function() { + sketch.createCanvas(100, 100); + }; + sketch.draw = function() {}; + }); + + assert.isNumber(p.frameCount); + assert.isAtLeast(p.frameCount, 0); + p.remove(); + }); + + test('pmouseX and pmouseY are numbers', function() { + const p = new p5((sketch) => { + sketch.setup = function() { + sketch.createCanvas(100, 100); + }; + sketch.draw = function() {}; + }); + + assert.isNumber(p.pmouseX); + assert.isNumber(p.pmouseY); + p.remove(); + }); + + test('focused is a boolean', function() { + const p = new p5((sketch) => { + sketch.setup = function() { + sketch.createCanvas(100, 100); + }; + sketch.draw = function() {}; + }); + + assert.isBoolean(p.focused); + p.remove(); + }); + + test('displayWidth and displayHeight are numbers', function() { + const p = new p5((sketch) => { + sketch.setup = function() { + sketch.createCanvas(100, 100); + }; + sketch.draw = function() {}; + }); + + assert.isNumber(p.displayWidth); + assert.isNumber(p.displayHeight); + p.remove(); + }); + + test('windowWidth and windowHeight are numbers', function() { + const p = new p5((sketch) => { + sketch.setup = function() { + sketch.createCanvas(100, 100); + }; + sketch.draw = function() {}; + }); + + assert.isNumber(p.windowWidth); + assert.isNumber(p.windowHeight); + p.remove(); + }); + + test('winMouseX and winMouseY are numbers', function() { + const p = new p5((sketch) => { + sketch.setup = function() { + sketch.createCanvas(100, 100); + }; + sketch.draw = function() {}; + }); + + assert.isNumber(p.winMouseX); + assert.isNumber(p.winMouseY); + p.remove(); + }); + + test('mouseIsPressed is a boolean', function() { + const p = new p5((sketch) => { + sketch.setup = function() { + sketch.createCanvas(100, 100); + }; + sketch.draw = function() {}; + }); + + assert.isBoolean(p.mouseIsPressed); + p.remove(); + }); + + test('all global properties are accessible', function() { + const p = new p5((sketch) => { + sketch.setup = function() { + sketch.createCanvas(100, 100); + }; + sketch.draw = function() {}; + }); + + assert.isDefined(p.width); + assert.isDefined(p.height); + assert.isDefined(p.mouseX); + assert.isDefined(p.mouseY); + assert.isDefined(p.pmouseX); + assert.isDefined(p.pmouseY); + assert.isDefined(p.winMouseX); + assert.isDefined(p.winMouseY); + assert.isDefined(p.frameCount); + assert.isDefined(p.focused); + assert.isDefined(p.displayWidth); + assert.isDefined(p.displayHeight); + assert.isDefined(p.windowWidth); + assert.isDefined(p.windowHeight); + assert.isDefined(p.mouseIsPressed); + + p.remove(); + }); +}); From 9a185ad0d990627141c5c4e5e24768d1ffa782a7 Mon Sep 17 00:00:00 2001 From: Perminder Singh <127239756+perminder-17@users.noreply.github.com> Date: Sun, 25 Jan 2026 23:07:15 +0530 Subject: [PATCH 03/11] Add builtin global accessors and enhance hooks --- src/strands/strands_api.js | 335 ++++++++++++++++++++++++------------- 1 file changed, 223 insertions(+), 112 deletions(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 092b98cd17..785085cb4f 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -9,16 +9,83 @@ import { isStructType, OpCode, StatementType, + NodeType, // isNativeType } from './ir_types' import { strandsBuiltinFunctions } from './strands_builtins' import { StrandsConditional } from './strands_conditionals' import { StrandsFor } from './strands_for' import * as CFG from './ir_cfg' +import * as DAG from './ir_dag'; import * as FES from './strands_FES' import { getNodeDataFromID } from './ir_dag' import { StrandsNode, createStrandsNode } from './strands_node' +const BUILTIN_GLOBAL_SPECS = { + mouseX: { typeInfo: DataType.float1, get: (p) => p.mouseX }, + width: { typeInfo: DataType.float1, get: (p) => p.width }, +} + +function _getBuiltinGlobalsCache(strandsContext) { + if (!strandsContext._builtinGlobals || strandsContext._builtinGlobals.dag !== strandsContext.dag) { + strandsContext._builtinGlobals = { + dag: strandsContext.dag, + nodes: new Map(), + uniformsAdded: new Set(), + } + } + // return the cache + return strandsContext._builtinGlobals +} + +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) + if (cached) return cached + + if (!cache.uniformsAdded.has(uniformName)) { + cache.uniformsAdded.add(uniformName) + strandsContext.uniforms.push({ + name: uniformName, + typeInfo: spec.typeInfo, + defaultValue: () => { + const p5Instance = strandsContext.renderer?._pInst || strandsContext.p5?.instance + return p5Instance ? spec.get(p5Instance) : undefined + }, + }) + } + + const { id, dimension } = build.variableNode(strandsContext, spec.typeInfo, uniformName) + const node = createStrandsNode(id, dimension, strandsContext) + node._originalBuiltinName = name + cache.nodes.set(uniformName, node) + return node +} + +function installBuiltinGlobalAccessors(strandsContext) { + if (strandsContext._builtinGlobalsAccessorsInstalled) return + + const getRuntimeP5Instance = () => strandsContext.renderer?._pInst || strandsContext.p5?.instance + + for (const name of Object.keys(BUILTIN_GLOBAL_SPECS)) { + const spec = BUILTIN_GLOBAL_SPECS[name] + Object.defineProperty(window, name, { + get: () => { + if (strandsContext.active) { + return getBuiltinGlobalNode(strandsContext, name); + } + const inst = getRuntimeP5Instance() + return spec.get(inst); + }, + }) + } + strandsContext._builtinGlobalsAccessorsInstalled = true +} + ////////////////////////////////////////////// // User nodes ////////////////////////////////////////////// @@ -63,6 +130,39 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return new StrandsFor(strandsContext, initialCb, conditionCb, updateCb, bodyCb, initialVars).build(); }; fn.strandsFor = p5.strandsFor; + p5.strandsEarlyReturn = function(value) { + const { dag, cfg } = strandsContext; + + // Ensure we're inside a hook + if (!strandsContext.activeHook) { + throw new Error('strandsEarlyReturn can only be used inside a hook callback'); + } + + // Convert value to a StrandsNode if it isn't already + const valueNode = value instanceof StrandsNode ? value : p5.strandsNode(value); + + // Create a new CFG block for the early return + const earlyReturnBlockID = CFG.createBasicBlock(cfg, BlockType.DEFAULT); + CFG.addEdge(cfg, cfg.currentBlock, earlyReturnBlockID); + CFG.pushBlock(cfg, earlyReturnBlockID); + + // Create the early return statement node + const nodeData = DAG.createNodeData({ + nodeType: NodeType.STATEMENT, + statementType: StatementType.EARLY_RETURN, + dependsOn: [valueNode.id] + }); + const earlyReturnID = DAG.getOrCreateNode(dag, nodeData); + CFG.recordInBasicBlock(cfg, cfg.currentBlock, earlyReturnID); + + // Add the value to the hook's earlyReturns array for later type checking + strandsContext.activeHook.earlyReturns.push({ earlyReturnID, valueNode }); + + CFG.popBlock(cfg); + + return valueNode; + }; + fn.strandsEarlyReturn = p5.strandsEarlyReturn; p5.strandsNode = function(...args) { if (args.length === 1 && args[0] instanceof StrandsNode) { return args[0]; @@ -130,7 +230,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { strandsContext._noiseOctaves = null; strandsContext._noiseAmpFalloff = null; - fn.noiseDetail = function (lod, falloff) { + fn.noiseDetail = function (lod, falloff = 0.5) { if (!strandsContext.active) { return originalNoiseDetail.apply(this, arguments); } @@ -280,72 +380,6 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } } } - ////////////////////////////////////////////// - // Global p5 properties - ////////////////////////////////////////////// - const globalProperties = [ - { name: 'width', type: DataType.float1 }, - { name: 'height', type: DataType.float1 }, - { name: 'mouseX', type: DataType.float1 }, - { name: 'mouseY', type: DataType.float1 }, - { name: 'pmouseX', type: DataType.float1 }, - { name: 'pmouseY', type: DataType.float1 }, - { name: 'winMouseX', type: DataType.float1 }, - { name: 'winMouseY', type: DataType.float1 }, - { name: 'frameCount', type: DataType.float1 }, - { name: 'focused', type: DataType.bool1 }, - { name: 'displayWidth', type: DataType.float1 }, - { name: 'displayHeight', type: DataType.float1 }, - { name: 'windowWidth', type: DataType.float1 }, - { name: 'windowHeight', type: DataType.float1 }, - { name: 'mouseIsPressed', type: DataType.bool1 } - ]; - - const originalDescriptors = {}; - for (const { name } of globalProperties) { - originalDescriptors[name] = Object.getOwnPropertyDescriptor(fn, name) || { - get: function() { return p5.prototype[name]; }, - configurable: true - }; - } - - for (const { name, type } of globalProperties) { - strandsContext.fnOverrides[name] = originalDescriptors[name]; - - (function(propName, propType, origDescriptor) { - Object.defineProperty(fn, propName, { - get: function() { - if (strandsContext.active) { - const uniformName = `_p5_${propName}`; - const existingUniform = strandsContext.uniforms.find(u => u.name === uniformName); - - if (!existingUniform) { - const { id, dimension } = build.variableNode(strandsContext, propType, uniformName); - strandsContext.uniforms.push({ - name: uniformName, - typeInfo: propType, - defaultValue: origDescriptor.get ? - () => origDescriptor.get.call(fn) : - () => origDescriptor.value - }); - return createStrandsNode(id, dimension, strandsContext); - } else { - const { id, dimension } = build.variableNode(strandsContext, propType, uniformName); - return createStrandsNode(id, dimension, strandsContext); - } - } else { - if (origDescriptor.get) { - return origDescriptor.get.call(this); - } else { - return origDescriptor.value; - } - } - }, - configurable: true, - enumerable: true - }); - })(name, type, originalDescriptors[name]); - } } ////////////////////////////////////////////// // Per-Hook functions @@ -357,7 +391,9 @@ function createHookArguments(strandsContext, parameters){ if(isStructType(param.type)) { const structTypeInfo = structType(param); const { id, dimension } = build.structInstanceNode(strandsContext, structTypeInfo, param.name, []); - const structNode = createStrandsNode(id, dimension, strandsContext); + const structNode = createStrandsNode(id, dimension, strandsContext).withStructProperties( + structTypeInfo.properties.map(prop => prop.name) + ); for (let i = 0; i < structTypeInfo.properties.length; i++) { const propertyType = structTypeInfo.properties[i]; Object.defineProperty(structNode, propertyType.name, { @@ -448,6 +484,8 @@ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName return returnedNodeID; } export function createShaderHooksFunctions(strandsContext, fn, shader) { + installBuiltinGlobalAccessors(strandsContext) + // Add shader context to hooks before spreading const vertexHooksWithContext = Object.fromEntries( Object.entries(shader.hooks.vertex).map(([name, hook]) => [name, { ...hook, shaderContext: 'vertex' }]) @@ -464,58 +502,110 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const { cfg, dag } = strandsContext; for (const hookType of hookTypes) { - const hookImplementation = function(hookUserCallback) { - const entryBlockID = CFG.createBasicBlock(cfg, BlockType.FUNCTION); + const hook = function(hookUserCallback) { + const args = setupHook(); + hook._result = hookUserCallback(...args) ?? hook._result; + finishHook(); + } + + // In the flat strands API, this is how result-returning hooks + // are used + hook.set = function(result) { + hook._result = result; + }; + + let entryBlockID; + function setupHook() { + strandsContext.activeHook = hook; + entryBlockID = CFG.createBasicBlock(cfg, BlockType.FUNCTION); CFG.addEdge(cfg, cfg.currentBlock, entryBlockID); CFG.pushBlock(cfg, entryBlockID); const args = createHookArguments(strandsContext, hookType.parameters); - const userReturned = hookUserCallback(...args); + const numStructArgs = hookType.parameters.filter(param => param.type.properties).length; + let argIdx = -1; + if (numStructArgs === 1) { + argIdx = hookType.parameters.findIndex(param => param.type.properties); + } + for (let i = 0; i < args.length; i++) { + if (i === argIdx) { + for (const key of args[argIdx].structProperties || []) { + Object.defineProperty(hook, key, { + get() { + return args[argIdx][key]; + }, + set(val) { + args[argIdx][key] = val; + }, + enumerable: true, + }); + } + if (hookType.returnType?.typeName === hookType.parameters[argIdx].type.typeName) { + hook.set(args[argIdx]); + } + } else { + hook[hookType.parameters[i].name] = args[i]; + } + } + return args; + }; + + function finishHook() { + const userReturned = hook._result; + strandsContext.activeHook = undefined; + const expectedReturnType = hookType.returnType; let rootNodeID = null; - if(isStructType(expectedReturnType)) { - const expectedStructType = structType(expectedReturnType); - if (userReturned instanceof StrandsNode) { - const returnedNode = getNodeDataFromID(strandsContext.dag, userReturned.id); - if (returnedNode.baseType !== expectedStructType.typeName) { - FES.userError("type error", `You have returned a ${userReturned.baseType} from ${hookType.name} when a ${expectedStructType.typeName} was expected.`); + const handleRetVal = (retNode) => { + if(isStructType(expectedReturnType)) { + const expectedStructType = structType(expectedReturnType); + if (retNode instanceof StrandsNode) { + const returnedNode = getNodeDataFromID(strandsContext.dag, retNode.id); + if (returnedNode.baseType !== expectedStructType.typeName) { + FES.userError("type error", `You have returned a ${retNode.baseType} from ${hookType.name} when a ${expectedStructType.typeName} was expected.`); + } + const newDeps = returnedNode.dependsOn.slice(); + for (let i = 0; i < expectedStructType.properties.length; i++) { + const expectedType = expectedStructType.properties[i].dataType; + const receivedNode = createStrandsNode(returnedNode.dependsOn[i], dag.dependsOn[retNode.id], strandsContext); + newDeps[i] = enforceReturnTypeMatch(strandsContext, expectedType, receivedNode, hookType.name); + } + dag.dependsOn[retNode.id] = newDeps; + return retNode.id; } - const newDeps = returnedNode.dependsOn.slice(); - for (let i = 0; i < expectedStructType.properties.length; i++) { - const expectedType = expectedStructType.properties[i].dataType; - const receivedNode = createStrandsNode(returnedNode.dependsOn[i], dag.dependsOn[userReturned.id], strandsContext); - newDeps[i] = enforceReturnTypeMatch(strandsContext, expectedType, receivedNode, hookType.name); + else { + const expectedProperties = expectedStructType.properties; + const newStructDependencies = []; + for (let i = 0; i < expectedProperties.length; i++) { + const expectedProp = expectedProperties[i]; + const propName = expectedProp.name; + const receivedValue = retNode[propName]; + if (receivedValue === undefined) { + FES.userError('type error', `You've returned an incomplete struct from ${hookType.name}.\n` + + `Expected: { ${expectedReturnType.properties.map(p => p.name).join(', ')} }\n` + + `Received: { ${Object.keys(retNode).join(', ')} }\n` + + `All of the properties are required!`); + } + const expectedTypeInfo = expectedProp.dataType; + const returnedPropID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, receivedValue, hookType.name); + newStructDependencies.push(returnedPropID); + } + const newStruct = build.structConstructorNode(strandsContext, expectedStructType, newStructDependencies); + return newStruct.id; } - dag.dependsOn[userReturned.id] = newDeps; - rootNodeID = userReturned.id; } - else { - const expectedProperties = expectedStructType.properties; - const newStructDependencies = []; - for (let i = 0; i < expectedProperties.length; i++) { - const expectedProp = expectedProperties[i]; - const propName = expectedProp.name; - const receivedValue = userReturned[propName]; - if (receivedValue === undefined) { - FES.userError('type error', `You've returned an incomplete struct from ${hookType.name}.\n` + - `Expected: { ${expectedReturnType.properties.map(p => p.name).join(', ')} }\n` + - `Received: { ${Object.keys(userReturned).join(', ')} }\n` + - `All of the properties are required!`); - } - const expectedTypeInfo = expectedProp.dataType; - const returnedPropID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, receivedValue, hookType.name); - newStructDependencies.push(returnedPropID); + else /*if(isNativeType(expectedReturnType.typeName))*/ { + if (!expectedReturnType.dataType) { + throw new Error(`Missing dataType for return type ${expectedReturnType.typeName}`); } - const newStruct = build.structConstructorNode(strandsContext, expectedStructType, newStructDependencies); - rootNodeID = newStruct.id; + const expectedTypeInfo = expectedReturnType.dataType; + return enforceReturnTypeMatch(strandsContext, expectedTypeInfo, retNode, hookType.name); } } - else /*if(isNativeType(expectedReturnType.typeName))*/ { - if (!expectedReturnType.dataType) { - throw new Error(`Missing dataType for return type ${expectedReturnType.typeName}`); - } - const expectedTypeInfo = expectedReturnType.dataType; - rootNodeID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, userReturned, hookType.name); + for (const { valueNode, earlyReturnID } of hook.earlyReturns) { + const id = handleRetVal(valueNode); + dag.dependsOn[earlyReturnID] = [id]; } + rootNodeID = userReturned ? handleRetVal(userReturned) : undefined; const fullHookName = `${hookType.returnType.typeName} ${hookType.name}`; const hookInfo = availableHooks[fullHookName]; strandsContext.hooks.push({ @@ -525,10 +615,31 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { shaderContext: hookInfo?.shaderContext, // 'vertex' or 'fragment' }); CFG.popBlock(cfg); + }; + hook.begin = setupHook; + hook.end = finishHook; + + const aliases = [hookType.name]; + if (strandsContext.baseShader?.hooks?.hookAliases?.[hookType.name]) { + aliases.push(...strandsContext.baseShader.hooks.hookAliases[hookType.name]); + } + + // If the hook has a name like getPixelInputs, create an alias without + // the get* prefix, like pixelInputs + const nameMatch = /^get([A-Z0-9]\w*)$/.exec(hookType.name); + if (nameMatch) { + const unprefixedName = nameMatch[1][0].toLowerCase() + nameMatch[1].slice(1); + if (!fn[unprefixedName]) { + aliases.push(unprefixedName); + } + } + + for (const name of aliases) { + strandsContext.windowOverrides[name] = window[name]; + strandsContext.fnOverrides[name] = fn[name]; + window[name] = hook; + fn[name] = hook; } - strandsContext.windowOverrides[hookType.name] = window[hookType.name]; - strandsContext.fnOverrides[hookType.name] = fn[hookType.name]; - window[hookType.name] = hookImplementation; - fn[hookType.name] = hookImplementation; + hook.earlyReturns = []; } } From c3933741163351cac1f84c322b98f1e82395e8c1 Mon Sep 17 00:00:00 2001 From: Perminder Singh <127239756+perminder-17@users.noreply.github.com> Date: Sun, 25 Jan 2026 23:12:13 +0530 Subject: [PATCH 04/11] Delete test/unit/webgl/strands.js --- test/unit/webgl/strands.js | 143 ------------------------------------- 1 file changed, 143 deletions(-) delete mode 100644 test/unit/webgl/strands.js diff --git a/test/unit/webgl/strands.js b/test/unit/webgl/strands.js deleted file mode 100644 index 0cff454779..0000000000 --- a/test/unit/webgl/strands.js +++ /dev/null @@ -1,143 +0,0 @@ -suite('p5.strands global properties', function() { - test('width and height are defined', function() { - const p = new p5((sketch) => { - sketch.setup = function() { - sketch.createCanvas(200, 150); - }; - sketch.draw = function() {}; - }); - - assert.equal(p.width, 200); - assert.equal(p.height, 150); - p.remove(); - }); - - test('mouseX and mouseY are numbers', function() { - const p = new p5((sketch) => { - sketch.setup = function() { - sketch.createCanvas(100, 100); - }; - sketch.draw = function() {}; - }); - - assert.isNumber(p.mouseX); - assert.isNumber(p.mouseY); - p.remove(); - }); - - test('frameCount is a number', function() { - const p = new p5((sketch) => { - sketch.setup = function() { - sketch.createCanvas(100, 100); - }; - sketch.draw = function() {}; - }); - - assert.isNumber(p.frameCount); - assert.isAtLeast(p.frameCount, 0); - p.remove(); - }); - - test('pmouseX and pmouseY are numbers', function() { - const p = new p5((sketch) => { - sketch.setup = function() { - sketch.createCanvas(100, 100); - }; - sketch.draw = function() {}; - }); - - assert.isNumber(p.pmouseX); - assert.isNumber(p.pmouseY); - p.remove(); - }); - - test('focused is a boolean', function() { - const p = new p5((sketch) => { - sketch.setup = function() { - sketch.createCanvas(100, 100); - }; - sketch.draw = function() {}; - }); - - assert.isBoolean(p.focused); - p.remove(); - }); - - test('displayWidth and displayHeight are numbers', function() { - const p = new p5((sketch) => { - sketch.setup = function() { - sketch.createCanvas(100, 100); - }; - sketch.draw = function() {}; - }); - - assert.isNumber(p.displayWidth); - assert.isNumber(p.displayHeight); - p.remove(); - }); - - test('windowWidth and windowHeight are numbers', function() { - const p = new p5((sketch) => { - sketch.setup = function() { - sketch.createCanvas(100, 100); - }; - sketch.draw = function() {}; - }); - - assert.isNumber(p.windowWidth); - assert.isNumber(p.windowHeight); - p.remove(); - }); - - test('winMouseX and winMouseY are numbers', function() { - const p = new p5((sketch) => { - sketch.setup = function() { - sketch.createCanvas(100, 100); - }; - sketch.draw = function() {}; - }); - - assert.isNumber(p.winMouseX); - assert.isNumber(p.winMouseY); - p.remove(); - }); - - test('mouseIsPressed is a boolean', function() { - const p = new p5((sketch) => { - sketch.setup = function() { - sketch.createCanvas(100, 100); - }; - sketch.draw = function() {}; - }); - - assert.isBoolean(p.mouseIsPressed); - p.remove(); - }); - - test('all global properties are accessible', function() { - const p = new p5((sketch) => { - sketch.setup = function() { - sketch.createCanvas(100, 100); - }; - sketch.draw = function() {}; - }); - - assert.isDefined(p.width); - assert.isDefined(p.height); - assert.isDefined(p.mouseX); - assert.isDefined(p.mouseY); - assert.isDefined(p.pmouseX); - assert.isDefined(p.pmouseY); - assert.isDefined(p.winMouseX); - assert.isDefined(p.winMouseY); - assert.isDefined(p.frameCount); - assert.isDefined(p.focused); - assert.isDefined(p.displayWidth); - assert.isDefined(p.displayHeight); - assert.isDefined(p.windowWidth); - assert.isDefined(p.windowHeight); - assert.isDefined(p.mouseIsPressed); - - p.remove(); - }); -}); From 17214976b8d237dbf197aad47ccbc92049139db5 Mon Sep 17 00:00:00 2001 From: Perminder Singh <127239756+perminder-17@users.noreply.github.com> Date: Sun, 25 Jan 2026 23:35:11 +0530 Subject: [PATCH 05/11] Add mouse and window properties to BUILTIN_GLOBAL_SPECS --- src/strands/strands_api.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 785085cb4f..c459374c74 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -22,8 +22,23 @@ import { getNodeDataFromID } from './ir_dag' import { StrandsNode, createStrandsNode } from './strands_node' const BUILTIN_GLOBAL_SPECS = { - mouseX: { typeInfo: DataType.float1, get: (p) => p.mouseX }, width: { typeInfo: DataType.float1, get: (p) => p.width }, + height: { typeInfo: DataType.float1, get: (p) => p.height }, + mouseX: { typeInfo: DataType.float1, get: (p) => p.mouseX }, + mouseY: { typeInfo: DataType.float1, get: (p) => p.mouseY }, + pmouseX: { typeInfo: DataType.float1, get: (p) => p.pmouseX }, + pmouseY: { typeInfo: DataType.float1, get: (p) => p.pmouseY }, + winMouseX: { typeInfo: DataType.float1, get: (p) => p.winMouseX }, + winMouseY: { typeInfo: DataType.float1, get: (p) => p.winMouseY }, + pwinMouseX: { typeInfo: DataType.float1, get: (p) => p.pwinMouseX }, + pwinMouseY: { typeInfo: DataType.float1, get: (p) => p.pwinMouseY }, + frameCount: { typeInfo: DataType.float1, get: (p) => p.frameCount }, + deltaTime: { typeInfo: DataType.float1, get: (p) => p.deltaTime }, + displayWidth: { typeInfo: DataType.float1, get: (p) => p.displayWidth }, + displayHeight: { typeInfo: DataType.float1, get: (p) => p.displayHeight }, + windowWidth: { typeInfo: DataType.float1, get: (p) => p.windowWidth }, + windowHeight: { typeInfo: DataType.float1, get: (p) => p.windowHeight }, + mouseIsPressed: { typeInfo: DataType.bool1, get: (p) => p.mouseIsPressed }, } function _getBuiltinGlobalsCache(strandsContext) { From df6cff1f941c57b900861c738445a34c2fdc8190 Mon Sep 17 00:00:00 2001 From: Perminder Singh <127239756+perminder-17@users.noreply.github.com> Date: Sun, 25 Jan 2026 23:50:15 +0530 Subject: [PATCH 06/11] Implement tests for global variables in shader hooks --- test/unit/webgl/p5.Shader.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index f520101be8..9f04ceb6f7 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -447,6 +447,28 @@ suite('p5.Shader', function() { }).not.toThrowError(); }); + test('returns numbers for builtin globals outside hooks and a strandNode when called inside hooks', () => { + myp5.createCanvas(5, 5, myp5.WEBGL); + let mxInHook; + let wInHook; + myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + mxInHook = window.mouseX; + wInHook = window.width; + inputs.color = [1, 0, 0, 1]; + return inputs; + }); + }, { myp5 }); + + const mx = window.mouseX; + const w = window.width; + assert.isTrue(mxInHook && mxInHook.isStrandsNode); + assert.isTrue(wInHook && wInHook.isStrandsNode); + assert.isNumber(mx); + assert.isNumber(w); + assert.strictEqual(w, myp5.width); + }); + test('handle custom uniform names with automatic values', () => { myp5.createCanvas(50, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => { From 30fcf4db5ee11bde27d4d866dea14aec912e455a Mon Sep 17 00:00:00 2001 From: Perminder Singh <127239756+perminder-17@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:06:57 +0530 Subject: [PATCH 07/11] Fixing tests Removed redundant assertions for mxInHook and wInHook. --- test/unit/webgl/p5.Shader.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 9f04ceb6f7..d6eb212790 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -456,14 +456,14 @@ suite('p5.Shader', function() { mxInHook = window.mouseX; wInHook = window.width; inputs.color = [1, 0, 0, 1]; + assert.isTrue(mxInHook && mxInHook.isStrandsNode); + assert.isTrue(wInHook && wInHook.isStrandsNode); return inputs; }); }, { myp5 }); const mx = window.mouseX; const w = window.width; - assert.isTrue(mxInHook && mxInHook.isStrandsNode); - assert.isTrue(wInHook && wInHook.isStrandsNode); assert.isNumber(mx); assert.isNumber(w); assert.strictEqual(w, myp5.width); From 6e5bd17bc016a5473cf9dbbc8e9eda709a544732 Mon Sep 17 00:00:00 2001 From: Perminder Singh <127239756+perminder-17@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:13:05 +0530 Subject: [PATCH 08/11] some minor fixes --- test/unit/webgl/p5.Shader.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index d6eb212790..12907a7383 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -456,8 +456,8 @@ suite('p5.Shader', function() { mxInHook = window.mouseX; wInHook = window.width; inputs.color = [1, 0, 0, 1]; - assert.isTrue(mxInHook && mxInHook.isStrandsNode); - assert.isTrue(wInHook && wInHook.isStrandsNode); + assert.isTrue(mxInHook.isStrandsNode); + assert.isTrue(wInHook.isStrandsNode); return inputs; }); }, { myp5 }); From 3db94cd0ed2c8b57b85481ae8a8d2e32ba3bcc2d Mon Sep 17 00:00:00 2001 From: Perminder Singh <127239756+perminder-17@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:15:54 +0530 Subject: [PATCH 09/11] Implement visual test for global property width and height Add visual test for getFinalColor using width/height --- test/unit/visual/cases/webgl.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index ad3a3998af..21f3b12f36 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -1022,6 +1022,24 @@ visualSuite('WebGL', function() { screenshot(); }); + visualTest('uses width/height in getFinalColor', (p5, screenshot) => { + let firstShader; + function firstShaderCallback() { + getFinalColor((color) => { + color = [width / 60, height / 60, 0, 1]; + return color; + }); + } + p5.createCanvas(60, 60, p5.WEBGL); + p5.pixelDensity(1); + firstShader = p5.baseColorShader().modify(firstShaderCallback); + p5.background(0); + p5.shader(firstShader); + p5.noStroke(); + p5.plane(20, 20); + 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 2fdd571654cad083f41545abe872064a54e8bf99 Mon Sep 17 00:00:00 2001 From: Perminder Singh Date: Tue, 27 Jan 2026 23:48:51 +0530 Subject: [PATCH 10/11] forgot to run tests locally --- .../uses width%2Fheight in getFinalColor/000.png | Bin 0 -> 284 bytes .../metadata.json | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/uses width%2Fheight in getFinalColor/000.png create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/uses width%2Fheight in getFinalColor/metadata.json diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/uses width%2Fheight in getFinalColor/000.png b/test/unit/visual/screenshots/WebGL/p5.strands/uses width%2Fheight in getFinalColor/000.png new file mode 100644 index 0000000000000000000000000000000000000000..668b80e93e458f064105c23f60308487d2771a9f GIT binary patch literal 284 zcmV+%0ptFOP)Cs+_aTKYL*F{p`mP*k0*j z2O@kJi2XVU+bccnK!gtiv0rDGaMwrRE+Kj#9L?L|5bhG92g1?39d4cQ3jhHB|K=r_ inE(I)21!IgR09CSVq`sC=kLP+0000 Date: Sun, 1 Feb 2026 03:15:34 +0530 Subject: [PATCH 11/11] strands specific fix --- test/unit/webgl/p5.Shader.js | 39 ++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 12907a7383..75e68257f2 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -447,27 +447,26 @@ suite('p5.Shader', function() { }).not.toThrowError(); }); - test('returns numbers for builtin globals outside hooks and a strandNode when called inside hooks', () => { - myp5.createCanvas(5, 5, myp5.WEBGL); - let mxInHook; - let wInHook; - myp5.baseMaterialShader().modify(() => { - myp5.getPixelInputs(inputs => { - mxInHook = window.mouseX; - wInHook = window.width; - inputs.color = [1, 0, 0, 1]; - assert.isTrue(mxInHook.isStrandsNode); - assert.isTrue(wInHook.isStrandsNode); - return inputs; - }); - }, { myp5 }); - - const mx = window.mouseX; - const w = window.width; - assert.isNumber(mx); - assert.isNumber(w); - assert.strictEqual(w, myp5.width); +test('returns numbers for builtin globals outside hooks and a strandNode when called inside hooks', () => { + myp5.createCanvas(5, 5, myp5.WEBGL); + myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + const mxInHook = window.mouseX; + const wInHook = window.width; + assert.isTrue(mxInHook.isStrandsNode); + assert.isTrue(wInHook.isStrandsNode); + inputs.color = [1, 0, 0, 1]; + return inputs; }); + }, { myp5 }); + + const mx = window.mouseX; + const w = window.width; + assert.isNumber(mx); + assert.isNumber(w); + assert.strictEqual(w, myp5.width); +}); + test('handle custom uniform names with automatic values', () => { myp5.createCanvas(50, 50, myp5.WEBGL);