Skip to content

Commit a213e53

Browse files
authored
Merge pull request #8367 from processing/fix/minified-strands
Fix p5.strands filters not working on minified code
2 parents 9512db6 + 3e6e25b commit a213e53

7 files changed

Lines changed: 156 additions & 84 deletions

File tree

src/core/filterShaders.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as constants from './constants';
66
export function makeFilterShader(renderer, operation, p5) {
77
switch (operation) {
88
case constants.GRAY:
9-
return renderer.baseFilterShader().modify(() => {
9+
return renderer.baseFilterShader().modify(({ p5 }) => {
1010
p5.getColor((inputs, canvasContent) => {
1111
const tex = p5.getTexture(canvasContent, inputs.texCoord);
1212
// weighted grayscale with luminance values
@@ -16,7 +16,7 @@ export function makeFilterShader(renderer, operation, p5) {
1616
}, { p5 });
1717

1818
case constants.INVERT:
19-
return renderer.baseFilterShader().modify(() => {
19+
return renderer.baseFilterShader().modify(({ p5 }) => {
2020
p5.getColor((inputs, canvasContent) => {
2121
const color = p5.getTexture(canvasContent, inputs.texCoord);
2222
const invertedColor = p5.vec3(1.0) - color.rgb;
@@ -25,7 +25,7 @@ export function makeFilterShader(renderer, operation, p5) {
2525
}, { p5 });
2626

2727
case constants.THRESHOLD:
28-
return renderer.baseFilterShader().modify(() => {
28+
return renderer.baseFilterShader().modify(({ p5 }) => {
2929
const filterParameter = p5.uniformFloat();
3030
p5.getColor((inputs, canvasContent) => {
3131
const color = p5.getTexture(canvasContent, inputs.texCoord);
@@ -38,7 +38,7 @@ export function makeFilterShader(renderer, operation, p5) {
3838
}, { p5 });
3939

4040
case constants.POSTERIZE:
41-
return renderer.baseFilterShader().modify(() => {
41+
return renderer.baseFilterShader().modify(({ p5 }) => {
4242
const filterParameter = p5.uniformFloat();
4343
const quantize = (color, n) => {
4444
// restrict values to N options/bins
@@ -60,7 +60,7 @@ export function makeFilterShader(renderer, operation, p5) {
6060
}, { p5 });
6161

6262
case constants.BLUR:
63-
return renderer.baseFilterShader().modify(() => {
63+
return renderer.baseFilterShader().modify(({ p5 }) => {
6464
const radius = p5.uniformFloat();
6565
const direction = p5.uniformVec2();
6666

@@ -120,7 +120,7 @@ export function makeFilterShader(renderer, operation, p5) {
120120
}, { p5 });
121121

122122
case constants.ERODE:
123-
return renderer.baseFilterShader().modify(() => {
123+
return renderer.baseFilterShader().modify(({ p5 }) => {
124124
const luma = (color) => {
125125
return p5.dot(color.rgb, p5.vec3(0.2126, 0.7152, 0.0722));
126126
};
@@ -150,7 +150,7 @@ export function makeFilterShader(renderer, operation, p5) {
150150
}, { p5 });
151151

152152
case constants.DILATE:
153-
return renderer.baseFilterShader().modify(() => {
153+
return renderer.baseFilterShader().modify(({ p5 }) => {
154154
const luma = (color) => {
155155
return p5.dot(color.rgb, p5.vec3(0.2126, 0.7152, 0.0722));
156156
};
@@ -180,7 +180,7 @@ export function makeFilterShader(renderer, operation, p5) {
180180
}, { p5 });
181181

182182
case constants.OPAQUE:
183-
return renderer.baseFilterShader().modify(() => {
183+
return renderer.baseFilterShader().modify(({ p5 }) => {
184184
p5.getColor((inputs, canvasContent) => {
185185
const color = p5.getTexture(canvasContent, inputs.texCoord);
186186
return p5.vec4(color.rgb, 1.0);

src/core/p5.Renderer3D.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1911,7 +1911,7 @@ export class Renderer3D extends Renderer {
19111911
_getSphereMapping(img) {
19121912
if (!this.sphereMapping) {
19131913
const p5 = this._pInst;
1914-
this.sphereMapping = this.baseFilterShader().modify(() => {
1914+
this.sphereMapping = this.baseFilterShader().modify(({ p5 }) => {
19151915
const uEnvMap = p5.uniformTexture();
19161916
const uFovY = p5.uniformFloat();
19171917
const uAspect = p5.uniformFloat();

src/strands/p5.strands.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ function strands(p5, fn) {
7575

7676
p5.Shader.prototype.modify = function (shaderModifier, scope = {}) {
7777
try {
78-
if (shaderModifier instanceof Function) {
78+
if (shaderModifier instanceof Function || typeof shaderModifier === 'string') {
7979
// Reset the context object every time modify is called;
8080
// const backend = glslBackend;
8181
initStrandsContext(strandsContext, this._renderer.strandsBackend, {
@@ -92,7 +92,9 @@ function strands(p5, fn) {
9292
if (options.parser) {
9393
// #7955 Wrap function declaration code in brackets so anonymous functions are not top level statements, which causes an error in acorn when parsing
9494
// https://github.com/acornjs/acorn/issues/1385
95-
const sourceString = `(${shaderModifier.toString()})`;
95+
const sourceString = typeof shaderModifier === 'string'
96+
? `(${shaderModifier})`
97+
: `(${shaderModifier.toString()})`;
9698
strandsCallback = transpileStrandsToJS(
9799
p5,
98100
sourceString,

src/strands/strands_for.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ export class StrandsFor {
359359
CFG.popBlock(cfg);
360360

361361
const loopVarNode = createStrandsNode(phiNode.id, phiNode.dimension, this.strandsContext);
362-
this.bodyResults = this.bodyCb(loopVarNode, phiVars);
362+
this.bodyResults = this.bodyCb(loopVarNode, phiVars) || {};
363363
for (const key in this.bodyResults) {
364364
this.bodyResults[key] = this.strandsContext.p5.strandsNode(this.bodyResults[key]);
365365
}

src/strands/strands_transpiler.js

Lines changed: 110 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -130,22 +130,19 @@ const ASTCallbacks = {
130130
if (ancestors.some(nodeIsUniform)) { return; }
131131
if (_state.varyings[node.name]
132132
&& !ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node)) {
133-
node.type = 'ExpressionStatement';
134-
node.expression = {
135-
type: 'CallExpression',
136-
callee: {
137-
type: 'MemberExpression',
138-
object: {
139-
type: 'Identifier',
140-
name: node.name
141-
},
142-
property: {
143-
type: 'Identifier',
144-
name: 'getValue'
145-
},
133+
node.type = 'CallExpression';
134+
node.callee = {
135+
type: 'MemberExpression',
136+
object: {
137+
type: 'Identifier',
138+
name: node.name
146139
},
147-
arguments: [],
148-
}
140+
property: {
141+
type: 'Identifier',
142+
name: 'getValue'
143+
},
144+
};
145+
node.arguments = [];
149146
}
150147
},
151148
// The callbacks for AssignmentExpression and BinaryExpression handle
@@ -208,13 +205,12 @@ const ASTCallbacks = {
208205
varyingName = node.left.object.name;
209206
}
210207
// Check if it's a getValue() call: myVarying.getValue().xyz
211-
else if (node.left.object.type === 'ExpressionStatement' &&
212-
node.left.object.expression?.type === 'CallExpression' &&
213-
node.left.object.expression.callee?.type === 'MemberExpression' &&
214-
node.left.object.expression.callee.property?.name === 'getValue' &&
215-
node.left.object.expression.callee.object?.type === 'Identifier' &&
216-
_state.varyings[node.left.object.expression.callee.object.name]) {
217-
varyingName = node.left.object.expression.callee.object.name;
208+
else if (node.left.object.type === 'CallExpression' &&
209+
node.left.object.callee?.type === 'MemberExpression' &&
210+
node.left.object.callee.property?.name === 'getValue' &&
211+
node.left.object.callee.object?.type === 'Identifier' &&
212+
_state.varyings[node.left.object.callee.object.name]) {
213+
varyingName = node.left.object.callee.object.name;
218214
}
219215

220216
if (varyingName) {
@@ -595,7 +591,7 @@ const ASTCallbacks = {
595591

596592
// Transform for statement into strandsFor() call
597593
// for (init; test; update) body -> strandsFor(initCb, conditionCb, updateCb, bodyCb, initialVars)
598-
594+
599595
// Generate unique loop variable name
600596
const uniqueLoopVar = `loopVar${loopVarCounter++}`;
601597

@@ -714,46 +710,71 @@ const ASTCallbacks = {
714710

715711
// Analyze which outer scope variables are assigned in the loop body
716712
const assignedVars = new Set();
717-
const analyzeBlock = (body, parentLocalVars = new Set()) => {
718-
if (body.type !== 'BlockStatement') return;
719713

720-
// First pass: collect variable declarations within this block
721-
const localVars = new Set([...parentLocalVars]);
722-
for (const stmt of body.body) {
714+
// Helper function to check if a block contains strands control flow calls as immediate children
715+
const blockContainsStrandsControlFlow = (node) => {
716+
if (node.type !== 'BlockStatement') return false;
717+
return node.body.some(stmt => {
718+
// Check for variable declarations with strands control flow init
723719
if (stmt.type === 'VariableDeclaration') {
724-
for (const decl of stmt.declarations) {
725-
if (decl.id.type === 'Identifier') {
726-
localVars.add(decl.id.name);
727-
}
728-
}
720+
const match = stmt.declarations.some(decl =>
721+
decl.init?.type === 'CallExpression' &&
722+
(
723+
(
724+
decl.init?.callee?.type === 'MemberExpression' &&
725+
decl.init?.callee?.object?.type === 'Identifier' &&
726+
decl.init?.callee?.object?.name === '__p5' &&
727+
(decl.init?.callee?.property?.name === 'strandsFor' ||
728+
decl.init?.callee?.property?.name === 'strandsIf')
729+
) ||
730+
(
731+
decl.init?.callee?.type === 'Identifier' &&
732+
(decl.init?.callee?.name === '__p5.strandsFor' ||
733+
decl.init?.callee?.name === '__p5.strandsIf')
734+
)
735+
)
736+
);
737+
return match
738+
}
739+
return false;
740+
});
741+
};
742+
743+
// First pass: collect all variable declarations in the body
744+
const localVars = new Set();
745+
ancestor(bodyFunction.body, {
746+
VariableDeclarator(node, ancestors) {
747+
// Skip if we're inside a block that contains strands control flow
748+
if (ancestors.some(blockContainsStrandsControlFlow)) return;
749+
if (node.id.type === 'Identifier') {
750+
localVars.add(node.id.name);
729751
}
730752
}
753+
});
731754

732-
// Second pass: find assignments to non-local variables
733-
for (const stmt of body.body) {
734-
if (stmt.type === 'ExpressionStatement' &&
735-
stmt.expression.type === 'AssignmentExpression') {
736-
const left = stmt.expression.left;
737-
if (left.type === 'Identifier') {
738-
// Direct variable assignment: x = value
739-
if (!localVars.has(left.name)) {
740-
assignedVars.add(left.name);
741-
}
742-
} else if (left.type === 'MemberExpression' &&
743-
left.object.type === 'Identifier') {
744-
// Property assignment: obj.prop = value (includes swizzles)
745-
if (!localVars.has(left.object.name)) {
746-
assignedVars.add(left.object.name);
747-
}
755+
// Second pass: find assignments to non-local variables using acorn-walk
756+
ancestor(bodyFunction.body, {
757+
AssignmentExpression(node, ancestors) {
758+
// Skip if we're inside a block that contains strands control flow
759+
if (ancestors.some(blockContainsStrandsControlFlow)) {
760+
return
761+
}
762+
763+
const left = node.left;
764+
if (left.type === 'Identifier') {
765+
// Direct variable assignment: x = value
766+
if (!localVars.has(left.name)) {
767+
assignedVars.add(left.name);
768+
}
769+
} else if (left.type === 'MemberExpression' &&
770+
left.object.type === 'Identifier') {
771+
// Property assignment: obj.prop = value (includes swizzles)
772+
if (!localVars.has(left.object.name)) {
773+
assignedVars.add(left.object.name);
748774
}
749-
} else if (stmt.type === 'BlockStatement') {
750-
// Recursively analyze nested block statements, passing down local vars
751-
analyzeBlock(stmt, localVars);
752775
}
753776
}
754-
};
755-
756-
analyzeBlock(bodyFunction.body);
777+
});
757778

758779
if (assignedVars.size > 0) {
759780
// Add copying, reference replacement, and return statements similar to if statements
@@ -959,7 +980,7 @@ const ASTCallbacks = {
959980
// Reset counters at the start of each transpilation
960981
blockVarCounter = 0;
961982
loopVarCounter = 0;
962-
983+
963984
const ast = parse(sourceString, {
964985
ecmaVersion: 2021,
965986
locations: srcLocations
@@ -992,18 +1013,37 @@ const ASTCallbacks = {
9921013
recursive(ast, { varyings: {} }, postOrderControlFlowTransform);
9931014
const transpiledSource = escodegen.generate(ast);
9941015
const scopeKeys = Object.keys(scope);
995-
const internalStrandsCallback = new Function(
996-
// Create a parameter called __p5, not just p5, because users of instance mode
997-
// may pass in a variable called p5 as a scope variable. If we rely on a variable called
998-
// p5, then the scope variable called p5 might accidentally override internal function
999-
// calls to p5 static methods.
1000-
'__p5',
1001-
...scopeKeys,
1002-
transpiledSource
1003-
.slice(
1004-
transpiledSource.indexOf('{') + 1,
1005-
transpiledSource.lastIndexOf('}')
1006-
).replaceAll(';', '')
1007-
);
1008-
return () => internalStrandsCallback(p5, ...scopeKeys.map(key => scope[key]));
1016+
const match = /\(?\s*(?:function)?\s*\(([^)]*)\)\s*(?:=>)?\s*{((?:.|\n)*)}\s*;?\s*\)?/
1017+
.exec(transpiledSource);
1018+
if (!match) {
1019+
console.log(transpiledSource);
1020+
throw new Error('Could not parse p5.strands function!');
1021+
}
1022+
const params = match[1].split(/,\s*/).filter(param => !!param.trim());
1023+
let paramVals, paramNames;
1024+
if (params.length > 0) {
1025+
paramNames = params;
1026+
paramVals = [scope];
1027+
} else {
1028+
paramNames = scopeKeys;
1029+
paramVals = scopeKeys.map(key => scope[key]);
1030+
}
1031+
const body = match[2];
1032+
try {
1033+
const internalStrandsCallback = new Function(
1034+
// Create a parameter called __p5, not just p5, because users of instance mode
1035+
// may pass in a variable called p5 as a scope variable. If we rely on a variable called
1036+
// p5, then the scope variable called p5 might accidentally override internal function
1037+
// calls to p5 static methods.
1038+
'__p5',
1039+
...paramNames,
1040+
body,
1041+
);
1042+
return () => internalStrandsCallback(p5, ...paramVals);
1043+
} catch (e) {
1044+
console.error(e);
1045+
console.log(paramNames);
1046+
console.log(body);
1047+
throw new Error('Error transpiling p5.strands callback!');
1048+
}
10091049
}

src/webgl/p5.Shader.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,8 @@ class Shader {
239239
* p5.strands functions are special, since they get turned into a shader instead of being
240240
* run like the rest of your code. They only have access to p5.js functions, and variables
241241
* you declare inside the `modify` callback. If you need access to local variables, you
242-
* can pass them into `modify` with an optional second parameter, `variables`. If you are
242+
* can pass them into `modify` with an optional second parameter, `variables`. These will
243+
* then be passed into your function as an argument. If you are
243244
* using instance mode, you will need to pass your sketch object in this way.
244245
*
245246
* ```js example
@@ -248,7 +249,7 @@ class Shader {
248249
*
249250
* sketch.setup = function() {
250251
* sketch.createCanvas(200, 200, sketch.WEBGL);
251-
* myShader = sketch.baseMaterialShader().modify(() => {
252+
* myShader = sketch.baseMaterialShader().modify(({ sketch }) => {
252253
* sketch.getPixelInputs((inputs) => {
253254
* inputs.color = [inputs.texCoord, 0, 1];
254255
* return inputs;

test/unit/webgl/p5.Shader.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,35 @@ suite('p5.Shader', function() {
958958
assert.approximately(pixelColor[2], 0, 5); // 0.0 * 255 = 0
959959
});
960960

961+
test('handle for loop modifying multiple variables after minification', () => {
962+
myp5.createCanvas(50, 50, myp5.WEBGL);
963+
964+
const testShader = myp5.baseMaterialShader().modify(() => {
965+
myp5.getPixelInputs(inputs => {
966+
let red = myp5.float(0.0);
967+
let green = myp5.float(0.0);
968+
969+
for (let i = 0; i < 4; i++) {
970+
// Note the comma!
971+
red = red + 0.125, // 4 * 0.125 = 0.5
972+
green = green + 0.25; // 4 * 0.25 = 1.0
973+
}
974+
975+
inputs.color = [red, green, 0.0, 1.0];
976+
return inputs;
977+
});
978+
}, { myp5 });
979+
980+
myp5.noStroke();
981+
myp5.shader(testShader);
982+
myp5.plane(myp5.width, myp5.height);
983+
984+
const pixelColor = myp5.get(25, 25);
985+
assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127
986+
assert.approximately(pixelColor[1], 255, 5); // 1.0 * 255 = 255
987+
assert.approximately(pixelColor[2], 0, 5); // 0.0 * 255 = 0
988+
});
989+
961990
test('handle for loop with conditional inside', () => {
962991
myp5.createCanvas(50, 50, myp5.WEBGL);
963992

0 commit comments

Comments
 (0)