Skip to content

Commit 00bf3c1

Browse files
Merge branch 'dev-2.0' into fix-createColorPicker-ignore-initial-color
2 parents 2271a30 + a213e53 commit 00bf3c1

7 files changed

Lines changed: 247 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: 141 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) {
@@ -275,6 +271,37 @@ const ASTCallbacks = {
275271
};
276272
node.arguments = [node.right];
277273
},
274+
LogicalExpression(node, _state, ancestors) {
275+
// Don't convert uniform default values to node methods, as
276+
// they should be evaluated at runtime, not compiled.
277+
if (ancestors.some(nodeIsUniform)) { return; }
278+
// If the left hand side of an expression is one of these types,
279+
// we should construct a node from it.
280+
const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
281+
if (unsafeTypes.includes(node.left.type)) {
282+
const leftReplacementNode = {
283+
type: 'CallExpression',
284+
callee: {
285+
type: 'Identifier',
286+
name: '__p5.strandsNode',
287+
},
288+
arguments: [node.left]
289+
}
290+
node.left = leftReplacementNode;
291+
}
292+
// Replace the logical operator with a call expression
293+
// in other words a call to BaseNode.or(), .and() etc.
294+
node.type = 'CallExpression';
295+
node.callee = {
296+
type: 'MemberExpression',
297+
object: node.left,
298+
property: {
299+
type: 'Identifier',
300+
name: replaceBinaryOperator(node.operator),
301+
},
302+
};
303+
node.arguments = [node.right];
304+
},
278305
IfStatement(node, _state, ancestors) {
279306
if (ancestors.some(nodeIsUniform)) { return; }
280307
// Transform if statement into strandsIf() call
@@ -564,7 +591,7 @@ const ASTCallbacks = {
564591

565592
// Transform for statement into strandsFor() call
566593
// for (init; test; update) body -> strandsFor(initCb, conditionCb, updateCb, bodyCb, initialVars)
567-
594+
568595
// Generate unique loop variable name
569596
const uniqueLoopVar = `loopVar${loopVarCounter++}`;
570597

@@ -683,46 +710,71 @@ const ASTCallbacks = {
683710

684711
// Analyze which outer scope variables are assigned in the loop body
685712
const assignedVars = new Set();
686-
const analyzeBlock = (body, parentLocalVars = new Set()) => {
687-
if (body.type !== 'BlockStatement') return;
688713

689-
// First pass: collect variable declarations within this block
690-
const localVars = new Set([...parentLocalVars]);
691-
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
692719
if (stmt.type === 'VariableDeclaration') {
693-
for (const decl of stmt.declarations) {
694-
if (decl.id.type === 'Identifier') {
695-
localVars.add(decl.id.name);
696-
}
697-
}
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);
698751
}
699752
}
753+
});
700754

701-
// Second pass: find assignments to non-local variables
702-
for (const stmt of body.body) {
703-
if (stmt.type === 'ExpressionStatement' &&
704-
stmt.expression.type === 'AssignmentExpression') {
705-
const left = stmt.expression.left;
706-
if (left.type === 'Identifier') {
707-
// Direct variable assignment: x = value
708-
if (!localVars.has(left.name)) {
709-
assignedVars.add(left.name);
710-
}
711-
} else if (left.type === 'MemberExpression' &&
712-
left.object.type === 'Identifier') {
713-
// Property assignment: obj.prop = value (includes swizzles)
714-
if (!localVars.has(left.object.name)) {
715-
assignedVars.add(left.object.name);
716-
}
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);
717774
}
718-
} else if (stmt.type === 'BlockStatement') {
719-
// Recursively analyze nested block statements, passing down local vars
720-
analyzeBlock(stmt, localVars);
721775
}
722776
}
723-
};
724-
725-
analyzeBlock(bodyFunction.body);
777+
});
726778

727779
if (assignedVars.size > 0) {
728780
// Add copying, reference replacement, and return statements similar to if statements
@@ -928,7 +980,7 @@ const ASTCallbacks = {
928980
// Reset counters at the start of each transpilation
929981
blockVarCounter = 0;
930982
loopVarCounter = 0;
931-
983+
932984
const ast = parse(sourceString, {
933985
ecmaVersion: 2021,
934986
locations: srcLocations
@@ -961,18 +1013,37 @@ const ASTCallbacks = {
9611013
recursive(ast, { varyings: {} }, postOrderControlFlowTransform);
9621014
const transpiledSource = escodegen.generate(ast);
9631015
const scopeKeys = Object.keys(scope);
964-
const internalStrandsCallback = new Function(
965-
// Create a parameter called __p5, not just p5, because users of instance mode
966-
// may pass in a variable called p5 as a scope variable. If we rely on a variable called
967-
// p5, then the scope variable called p5 might accidentally override internal function
968-
// calls to p5 static methods.
969-
'__p5',
970-
...scopeKeys,
971-
transpiledSource
972-
.slice(
973-
transpiledSource.indexOf('{') + 1,
974-
transpiledSource.lastIndexOf('}')
975-
).replaceAll(';', '')
976-
);
977-
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+
}
9781049
}

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;

0 commit comments

Comments
 (0)