From b3956eacf935275029668a32e7976b7c84379e54 Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Wed, 25 Feb 2026 21:04:31 +0530 Subject: [PATCH 1/3] Fix filter() crash on createGraphics(WEBGL) by mirroring strands API onto p5.Graphics.prototype --- src/strands/p5.strands.js | 12 +++++ src/strands/strands_api.js | 101 ++++++++++++++++++++++++------------- 2 files changed, 79 insertions(+), 34 deletions(-) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 9eb2269a13..a88e06ad30 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -49,6 +49,7 @@ function strands(p5, fn) { ctx.previousFES = p5.disableFriendlyErrors; ctx.windowOverrides = {}; ctx.fnOverrides = {}; + ctx.graphicsOverrides = {}; if (active) { p5.disableFriendlyErrors = true; } @@ -71,6 +72,17 @@ function strands(p5, fn) { for (const key in ctx.fnOverrides) { fn[key] = ctx.fnOverrides[key]; } + // Clean up the hooks temporarily installed on p5.Graphics.prototype (#8549) + const GraphicsProto = p5.Graphics?.prototype; + if (GraphicsProto) { + for (const key in ctx.graphicsOverrides) { + if (ctx.graphicsOverrides[key] === undefined) { + delete GraphicsProto[key]; + } else { + GraphicsProto[key] = ctx.graphicsOverrides[key]; + } + } + } } const strandsContext = {}; diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 29e597b584..be658a9c61 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -114,6 +114,39 @@ function installBuiltinGlobalAccessors(strandsContext) { strandsContext._builtinGlobalsAccessorsInstalled = true } +////////////////////////////////////////////// +// Prototype mirroring helpers +////////////////////////////////////////////// + +/** + * Permanently augment both p5.prototype (fn) and p5.Graphics.prototype + * with a strands function. Overwrites unconditionally - strands wrappers + * are the correct dual mode implementation. + */ +function augmentFn(fn, p5, name, value) { + fn[name] = value; + const GraphicsProto = p5?.Graphics?.prototype; + if (GraphicsProto) { + GraphicsProto[name] = value; + } +} + +/** + * Temporarily augment both p5.prototype (fn) and p5.Graphics.prototype + * with a hook function. Saves the previous own property value in + * overridesStore so deinitStrandsContext can restore it. + */ +function augmentFnTemporary(fn, p5, name, value, overridesStore) { + fn[name] = value; + const GraphicsProto = p5?.Graphics?.prototype; + if (GraphicsProto) { + overridesStore[name] = Object.prototype.hasOwnProperty.call(GraphicsProto, name) + ? GraphicsProto[name] + : undefined; + GraphicsProto[name] = value; + } +} + ////////////////////////////////////////////// // User nodes ////////////////////////////////////////////// @@ -137,27 +170,27 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { ////////////////////////////////////////////// // Unique Functions ////////////////////////////////////////////// - fn.discard = function() { + augmentFn(fn, p5, 'discard', function() { build.statementNode(strandsContext, StatementType.DISCARD); - } - fn.break = function() { + }); + augmentFn(fn, p5, 'break', function() { build.statementNode(strandsContext, StatementType.BREAK); - }; + }); p5.break = fn.break; - fn.instanceID = function() { + augmentFn(fn, p5, 'instanceID', function() { const node = build.variableNode(strandsContext, { baseType: BaseType.INT, dimension: 1 }, strandsContext.backend.instanceIdReference()); return createStrandsNode(node.id, node.dimension, strandsContext); - } + }); // Internal methods use p5 static methods; user-facing methods use fn. // Some methods need to be used by both. p5.strandsIf = function(conditionNode, ifBody) { return new StrandsConditional(strandsContext, conditionNode, ifBody); } - fn.strandsIf = p5.strandsIf; + augmentFn(fn, p5, 'strandsIf', p5.strandsIf); p5.strandsFor = function(initialCb, conditionCb, updateCb, bodyCb, initialVars) { return new StrandsFor(strandsContext, initialCb, conditionCb, updateCb, bodyCb, initialVars).build(); }; - fn.strandsFor = p5.strandsFor; + augmentFn(fn, p5, 'strandsFor', p5.strandsFor); p5.strandsEarlyReturn = function(value) { const { dag, cfg } = strandsContext; @@ -190,7 +223,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return valueNode; }; - fn.strandsEarlyReturn = p5.strandsEarlyReturn; + augmentFn(fn, p5, 'strandsEarlyReturn', p5.strandsEarlyReturn); p5.strandsNode = function(...args) { if (args.length === 1 && args[0] instanceof StrandsNode) { return args[0]; @@ -221,16 +254,16 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const isp5Function = overrides[0].isp5Function; if (isp5Function) { const originalFn = fn[functionName]; - fn[functionName] = function(...args) { + augmentFn(fn, p5, functionName, function(...args) { if (strandsContext.active) { const { id, dimension } = build.functionCallNode(strandsContext, functionName, args); return createStrandsNode(id, dimension, strandsContext); } else { return originalFn.apply(this, args); } - } + }); } else { - fn[functionName] = function (...args) { + augmentFn(fn, p5, functionName, function (...args) { if (strandsContext.active) { const { id, dimension } = build.functionCallNode(strandsContext, functionName, args); return createStrandsNode(id, dimension, strandsContext); @@ -239,11 +272,11 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { `It looks like you've called ${functionName} outside of a shader's modify() function.` ) } - } + }); } } - fn.getTexture = function (...rawArgs) { + augmentFn(fn, p5, 'getTexture', function (...rawArgs) { if (strandsContext.active) { const { id, dimension } = strandsContext.backend.createGetTextureCall(strandsContext, rawArgs); return createStrandsNode(id, dimension, strandsContext); @@ -252,17 +285,17 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { `It looks like you've called getTexture outside of a shader's modify() function.` ) } - } + }); // Add texture function as alias for getTexture with p5 fallback const originalTexture = fn.texture; - fn.texture = function (...args) { + augmentFn(fn, p5, 'texture', function (...args) { if (strandsContext.active) { return this.getTexture(...args); } else { return originalTexture.apply(this, args); } - } + }); // Add noise function with backend-agnostic implementation const originalNoise = fn.noise; @@ -272,16 +305,16 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { strandsContext._noiseOctaves = null; strandsContext._noiseAmpFalloff = null; - fn.noiseDetail = function (lod, falloff = 0.5) { + augmentFn(fn, p5, 'noiseDetail', function (lod, falloff = 0.5) { if (!strandsContext.active) { return originalNoiseDetail.apply(this, arguments); } strandsContext._noiseOctaves = lod; strandsContext._noiseAmpFalloff = falloff; - }; + }); - fn.noise = function (...args) { + augmentFn(fn, p5, 'noise', function (...args) { if (!strandsContext.active) { return originalNoise.apply(this, args); // fallback to regular p5.js noise } @@ -328,9 +361,9 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }] }); return createStrandsNode(id, dimension, strandsContext); - }; + }); - fn.millis = function (...args) { + augmentFn(fn, p5, 'millis', function (...args) { if (!strandsContext.active) { return originalMillis.apply(this, args); } @@ -343,7 +376,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return instance ? instance.millis() : undefined; } ); - }; + }); // Next is type constructors and uniform functions. // For some of them, we have aliases so that you can write either a more human-readable @@ -372,13 +405,13 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { typeAliases.push(pascalTypeName.replace('Vec', 'Vector')); } } - fn[`uniform${pascalTypeName}`] = function(name, defaultValue) { + augmentFn(fn, p5, `uniform${pascalTypeName}`, function(name, defaultValue) { const { id, dimension } = build.variableNode(strandsContext, typeInfo, name); strandsContext.uniforms.push({ name, typeInfo, defaultValue }); return createStrandsNode(id, dimension, strandsContext); - }; + }); // Shared variables with smart context detection - fn[`shared${pascalTypeName}`] = function(name) { + augmentFn(fn, p5, `shared${pascalTypeName}`, function(name) { const { id, dimension } = build.variableNode(strandsContext, typeInfo, name); // Initialize shared variables tracking if not present @@ -395,20 +428,20 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }); return createStrandsNode(id, dimension, strandsContext); - }; + }); // Alias varying* as shared* for backward compatibility - fn[`varying${pascalTypeName}`] = fn[`shared${pascalTypeName}`]; + augmentFn(fn, p5, `varying${pascalTypeName}`, fn[`shared${pascalTypeName}`]); for (const typeAlias of typeAliases) { // For compatibility, also alias uniformVec2 as uniformVector2, what we initially // documented these as - fn[`uniform${typeAlias}`] = fn[`uniform${pascalTypeName}`]; - fn[`varying${typeAlias}`] = fn[`varying${pascalTypeName}`]; - fn[`shared${typeAlias}`] = fn[`shared${pascalTypeName}`]; + augmentFn(fn, p5, `uniform${typeAlias}`, fn[`uniform${pascalTypeName}`]); + augmentFn(fn, p5, `varying${typeAlias}`, fn[`varying${pascalTypeName}`]); + augmentFn(fn, p5, `shared${typeAlias}`, fn[`shared${pascalTypeName}`]); } const originalp5Fn = fn[typeInfo.fnName]; - fn[typeInfo.fnName] = function(...args) { + augmentFn(fn, p5, typeInfo.fnName, function(...args) { if (strandsContext.active) { if (args.length === 1 && args[0].dimension && args[0].dimension === typeInfo.dimension) { const { id, dimension } = build.functionCallNode( @@ -440,7 +473,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { `It looks like you've called ${typeInfo.fnName} outside of a shader's modify() function.` ); } - } + }); } } ////////////////////////////////////////////// @@ -726,7 +759,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { strandsContext.windowOverrides[name] = window[name]; strandsContext.fnOverrides[name] = fn[name]; window[name] = hook; - fn[name] = hook; + augmentFnTemporary(fn, strandsContext.p5, name, hook, strandsContext.graphicsOverrides); } hook.earlyReturns = []; } From b12952cc20b6594630ff33f97fd930d5bc073818 Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Fri, 27 Feb 2026 02:05:39 +0530 Subject: [PATCH 2/3] Refractor override handling and add visual regression test --- src/strands/strands_api.js | 14 +++++++------- test/unit/visual/cases/webgl.js | 17 +++++++++++++++++ .../On a createGraphics WEBGL buffer/000.png | Bin 0 -> 577 bytes .../metadata.json | 3 +++ 4 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGL/filter/On a createGraphics WEBGL buffer/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/On a createGraphics WEBGL buffer/metadata.json diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index be658a9c61..833868b5c4 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -118,7 +118,7 @@ function installBuiltinGlobalAccessors(strandsContext) { // Prototype mirroring helpers ////////////////////////////////////////////// -/** +/* * Permanently augment both p5.prototype (fn) and p5.Graphics.prototype * with a strands function. Overwrites unconditionally - strands wrappers * are the correct dual mode implementation. @@ -131,16 +131,16 @@ function augmentFn(fn, p5, name, value) { } } -/** +/* * Temporarily augment both p5.prototype (fn) and p5.Graphics.prototype * with a hook function. Saves the previous own property value in - * overridesStore so deinitStrandsContext can restore it. + * graphicsOverrides so deinitStrandsContext can restore it. */ -function augmentFnTemporary(fn, p5, name, value, overridesStore) { +function augmentFnTemporary(fn, strandsContext, name, value) { fn[name] = value; - const GraphicsProto = p5?.Graphics?.prototype; + const GraphicsProto = strandsContext.p5?.Graphics?.prototype; if (GraphicsProto) { - overridesStore[name] = Object.prototype.hasOwnProperty.call(GraphicsProto, name) + strandsContext.graphicsOverrides[name] = Object.prototype.hasOwnProperty.call(GraphicsProto, name) ? GraphicsProto[name] : undefined; GraphicsProto[name] = value; @@ -759,7 +759,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { strandsContext.windowOverrides[name] = window[name]; strandsContext.fnOverrides[name] = fn[name]; window[name] = hook; - augmentFnTemporary(fn, strandsContext.p5, name, hook, strandsContext.graphicsOverrides); + augmentFnTemporary(fn, strandsContext, name, hook); } hook.earlyReturns = []; } diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 52de295d16..d87374fa88 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -245,6 +245,23 @@ visualSuite('WebGL', function() { }); }); } + + visualTest('On a createGraphics WEBGL buffer', function(p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + + const g = p5.createGraphics(50, 50, p5.WEBGL); + g.background(255); + g.noStroke(); + g.fill('red'); + g.circle(0, 0, 30); + + g.filter(p5.INVERT); + + p5.imageMode(p5.CENTER); + p5.image(g, 0, 0); + + screenshot(); + }); }); visualSuite('Lights', function() { diff --git a/test/unit/visual/screenshots/WebGL/filter/On a createGraphics WEBGL buffer/000.png b/test/unit/visual/screenshots/WebGL/filter/On a createGraphics WEBGL buffer/000.png new file mode 100644 index 0000000000000000000000000000000000000000..87bd2084a17ce980dfce74161967963e7d803358 GIT binary patch literal 577 zcmV-H0>1r;P)2G0e#rPR4LDekM@_MQUqr90({?yv@emv7g;m{RiOvFPK3lj^BTsAV-_w zIgxWBJ9XgvXzkS4VcBycS6deWJU;`kuZz7;e;jP<-oA*d$44D~6LEoS!6Xm?M10g0 z0Ym`g0%)*^5C6uy?I0@vSt})?6=)lXA;=oQ*XW1^$XZ{0h5a~)6O)BhcS6bKyLBh* zntKeS8)B`F9EPcbL;&QPy8oRVf~kXe8vW&8Dw3D`#I6Ps0YoHZ8$yZ=tAY5pzoEvI z1epq3gYqB|>zQls{m&!Pnt714WMkmtV_GZ^QvA%OE4URHam#z*%Y#H*96Z`45AqY0 zN2IeJ8zhYZ92-PRqvS!PwCGO+9vehT_aqPU_LdfH3oPW!gGg=lrGz9vN`*Qo58`Oj z>-X5pODRtcq$pmhk5+Kd)c_9IE0dE4Bb*H5dWD=V%X(`k|tGQ^KVQtbJdSfZO`a#Mi^c*-r zdivO>a!#bLmpw}7M0)zzr*clDua`Ya{{a91|NjXoNu~e*00v1!K~w_(HPfy$aSRyc P00000NkvXXu0mjfYHRiE literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/filter/On a createGraphics WEBGL buffer/metadata.json b/test/unit/visual/screenshots/WebGL/filter/On a createGraphics WEBGL buffer/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/On a createGraphics WEBGL buffer/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file From acbc2287755685b2d256da80e1f9f845a2910c7b Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Sat, 28 Feb 2026 21:34:01 +0530 Subject: [PATCH 3/3] Refractor temporary override handling for strands hooks --- src/strands/strands_api.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 833868b5c4..ef4c0424c3 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -132,11 +132,14 @@ function augmentFn(fn, p5, name, value) { } /* - * Temporarily augment both p5.prototype (fn) and p5.Graphics.prototype - * with a hook function. Saves the previous own property value in - * graphicsOverrides so deinitStrandsContext can restore it. + * Temporarily augment window, p5.prototype (fn), and p5.Graphics.prototype + * with a hook function. Saves previous values into strandsContext override + * stores so deinitStrandsContext can restore them. */ function augmentFnTemporary(fn, strandsContext, name, value) { + strandsContext.windowOverrides[name] = window[name]; + strandsContext.fnOverrides[name] = fn[name]; + window[name] = value; fn[name] = value; const GraphicsProto = strandsContext.p5?.Graphics?.prototype; if (GraphicsProto) { @@ -756,9 +759,6 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { } for (const name of aliases) { - strandsContext.windowOverrides[name] = window[name]; - strandsContext.fnOverrides[name] = fn[name]; - window[name] = hook; augmentFnTemporary(fn, strandsContext, name, hook); } hook.earlyReturns = [];