p5 2.2.3 → 2.3.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dist/accessibility/color_namer.js +9 -11
  2. package/dist/accessibility/describe.js +0 -1
  3. package/dist/accessibility/gridOutput.js +0 -1
  4. package/dist/accessibility/index.js +9 -10
  5. package/dist/accessibility/outputs.js +0 -1
  6. package/dist/accessibility/textOutput.js +0 -1
  7. package/dist/app.js +11 -10
  8. package/dist/app.node.js +122 -0
  9. package/dist/color/color_conversion.js +9 -11
  10. package/dist/color/creating_reading.js +1 -1
  11. package/dist/color/index.js +2 -2
  12. package/dist/color/p5.Color.js +1 -1
  13. package/dist/color/setting.js +25 -12
  14. package/dist/{constants-BdTiYOQI.js → constants-CYF6mp5_.js} +2 -2
  15. package/dist/core/States.js +1 -1
  16. package/dist/core/constants.js +1 -1
  17. package/dist/core/environment.js +28 -29
  18. package/dist/core/filterShaders.js +1 -1
  19. package/dist/core/friendly_errors/fes_core.js +9 -8
  20. package/dist/core/friendly_errors/file_errors.js +1 -2
  21. package/dist/core/friendly_errors/index.js +1 -1
  22. package/dist/core/friendly_errors/param_validator.js +737 -640
  23. package/dist/core/friendly_errors/sketch_verifier.js +1 -1
  24. package/dist/core/friendly_errors/stacktrace.js +0 -1
  25. package/dist/core/helpers.js +3 -4
  26. package/dist/core/init.js +24 -21
  27. package/dist/core/internationalization.js +1 -1
  28. package/dist/core/legacy.js +9 -11
  29. package/dist/core/main.js +9 -10
  30. package/dist/core/p5.Graphics.js +5 -5
  31. package/dist/core/p5.Renderer.js +3 -3
  32. package/dist/core/p5.Renderer2D.js +9 -10
  33. package/dist/core/p5.Renderer3D.js +5 -5
  34. package/dist/core/rendering.js +5 -5
  35. package/dist/core/structure.js +0 -1
  36. package/dist/core/transform.js +7 -16
  37. package/dist/{creating_reading-C7hu6sg1.js → creating_reading-DLkHH80h.js} +11 -8
  38. package/dist/data/local_storage.js +0 -1
  39. package/dist/dom/dom.js +2 -3
  40. package/dist/dom/index.js +2 -2
  41. package/dist/dom/p5.Element.js +2 -2
  42. package/dist/dom/p5.MediaElement.js +2 -2
  43. package/dist/events/acceleration.js +5 -3
  44. package/dist/events/keyboard.js +0 -1
  45. package/dist/events/pointer.js +0 -2
  46. package/dist/image/const.js +1 -1
  47. package/dist/image/filterRenderer2D.js +19 -12
  48. package/dist/image/image.js +5 -5
  49. package/dist/image/index.js +5 -5
  50. package/dist/image/loading_displaying.js +5 -5
  51. package/dist/image/p5.Image.js +3 -3
  52. package/dist/image/pixels.js +0 -1
  53. package/dist/io/files.js +5 -5
  54. package/dist/io/index.js +5 -5
  55. package/dist/io/p5.Table.js +0 -1
  56. package/dist/io/p5.TableRow.js +0 -1
  57. package/dist/io/p5.XML.js +0 -1
  58. package/dist/{ir_builders-Cd6rU9Vm.js → ir_builders-C2ebb6Lu.js} +234 -1
  59. package/dist/{main-H_nu4eDs.js → main-D2MtO721.js} +107 -136
  60. package/dist/math/Matrices/Matrix.js +1 -1
  61. package/dist/math/Matrices/MatrixNumjs.js +1 -1
  62. package/dist/math/calculation.js +0 -1
  63. package/dist/math/index.js +3 -1
  64. package/dist/math/math.js +3 -17
  65. package/dist/math/noise.js +0 -1
  66. package/dist/math/p5.Matrix.js +1 -2
  67. package/dist/math/p5.Vector.js +233 -279
  68. package/dist/math/patch-vector.js +75 -0
  69. package/dist/math/random.js +0 -1
  70. package/dist/math/trigonometry.js +3 -4
  71. package/dist/{p5.Renderer-BmD2P6Wv.js → p5.Renderer-C0Kzy71d.js} +31 -24
  72. package/dist/{rendering-CC8JNTwG.js → rendering-CvNr0bB8.js} +732 -44
  73. package/dist/shape/2d_primitives.js +1 -4
  74. package/dist/shape/attributes.js +43 -8
  75. package/dist/shape/curves.js +0 -1
  76. package/dist/shape/custom_shapes.js +260 -5
  77. package/dist/shape/index.js +2 -2
  78. package/dist/shape/vertex.js +0 -2
  79. package/dist/strands/ir_builders.js +1 -1
  80. package/dist/strands/ir_types.js +5 -1
  81. package/dist/strands/p5.strands.js +286 -31
  82. package/dist/strands/strands_api.js +179 -8
  83. package/dist/strands/strands_codegen.js +26 -8
  84. package/dist/strands/strands_conditionals.js +1 -1
  85. package/dist/strands/strands_for.js +1 -1
  86. package/dist/strands/strands_node.js +1 -1
  87. package/dist/strands/strands_ternary.js +56 -0
  88. package/dist/strands/strands_transpiler.js +416 -251
  89. package/dist/strands_glslBackend-i-ReKgZo.js +423 -0
  90. package/dist/type/index.js +3 -3
  91. package/dist/type/lib/Typr.js +1 -1
  92. package/dist/type/p5.Font.js +3 -3
  93. package/dist/type/textCore.js +31 -24
  94. package/dist/utilities/conversion.js +0 -1
  95. package/dist/utilities/time_date.js +0 -1
  96. package/dist/utilities/utility_functions.js +0 -1
  97. package/dist/webgl/3d_primitives.js +5 -5
  98. package/dist/webgl/GeometryBuilder.js +1 -1
  99. package/dist/webgl/ShapeBuilder.js +26 -1
  100. package/dist/webgl/enums.js +1 -1
  101. package/dist/webgl/index.js +8 -9
  102. package/dist/webgl/interaction.js +8 -4
  103. package/dist/webgl/light.js +5 -5
  104. package/dist/webgl/loading.js +60 -21
  105. package/dist/webgl/material.js +5 -5
  106. package/dist/webgl/p5.Camera.js +5 -5
  107. package/dist/webgl/p5.Framebuffer.js +5 -5
  108. package/dist/webgl/p5.Geometry.js +3 -5
  109. package/dist/webgl/p5.Quat.js +1 -1
  110. package/dist/webgl/p5.RendererGL.js +17 -21
  111. package/dist/webgl/p5.Shader.js +129 -36
  112. package/dist/webgl/p5.Texture.js +5 -5
  113. package/dist/webgl/strands_glslBackend.js +5 -386
  114. package/dist/webgl/text.js +5 -5
  115. package/dist/webgl/utils.js +5 -5
  116. package/dist/webgl2Compatibility-DA7DLMuq.js +7 -0
  117. package/dist/webgpu/index.js +7 -3
  118. package/dist/webgpu/p5.RendererWebGPU.js +1036 -180
  119. package/dist/webgpu/shaders/color.js +1 -1
  120. package/dist/webgpu/shaders/compute.js +32 -0
  121. package/dist/webgpu/shaders/functions/randomComputeWGSL.js +31 -0
  122. package/dist/webgpu/shaders/functions/randomVertWGSL.js +30 -0
  123. package/dist/webgpu/shaders/functions/randomWGSL.js +30 -0
  124. package/dist/webgpu/shaders/line.js +1 -1
  125. package/dist/webgpu/shaders/material.js +3 -3
  126. package/dist/webgpu/strands_wgslBackend.js +137 -15
  127. package/lib/p5.esm.js +4088 -1950
  128. package/lib/p5.esm.min.js +1 -1
  129. package/lib/p5.js +4088 -1950
  130. package/lib/p5.min.js +1 -1
  131. package/lib/p5.webgpu.esm.js +1638 -306
  132. package/lib/p5.webgpu.js +1637 -305
  133. package/lib/p5.webgpu.min.js +1 -1
  134. package/package.json +6 -1
  135. package/types/global.d.ts +4137 -2396
  136. package/types/p5.d.ts +2702 -1658
  137. package/dist/noise3DGLSL-Bwrdi4gi.js +0 -9
@@ -2,6 +2,7 @@ import { parse } from 'acorn';
2
2
  import { ancestor, recursive } from 'acorn-walk';
3
3
  import escodegen from 'escodegen';
4
4
  import { UnarySymbolToName } from './ir_types.js';
5
+ import { internalError } from './strands_FES.js';
5
6
 
6
7
  let blockVarCounter = 0;
7
8
  let loopVarCounter = 0;
@@ -41,6 +42,50 @@ function nodeIsUniform(ancestor) {
41
42
  );
42
43
  }
43
44
 
45
+ function nodeIsUniformCallbackFn(node, names) {
46
+ if (!names?.size) return false;
47
+ if (node.type === 'FunctionDeclaration' && names.has(node.id?.name)) return true;
48
+ if (
49
+ node.type === 'VariableDeclarator' && names.has(node.id?.name) &&
50
+ (node.init?.type === 'FunctionExpression' || node.init?.type === 'ArrowFunctionExpression')
51
+ ) {
52
+ return true;
53
+ }
54
+ return false;
55
+ }
56
+
57
+ function collectUniformCallbackNames(ast) {
58
+ // Sub-pass 1: collect all named function definitions
59
+ const namedFunctions = new Set();
60
+ ancestor(ast, {
61
+ FunctionDeclaration(node) {
62
+ if (node.id) namedFunctions.add(node.id.name);
63
+ },
64
+ VariableDeclarator(node) {
65
+ if (
66
+ node.id?.type === 'Identifier' &&
67
+ (node.init?.type === 'FunctionExpression' || node.init?.type === 'ArrowFunctionExpression')
68
+ ) {
69
+ namedFunctions.add(node.id.name);
70
+ }
71
+ }
72
+ });
73
+ // Sub-pass 2: find which of those names are passed as uniform call arguments
74
+ const names = new Set();
75
+ ancestor(ast, {
76
+ CallExpression(node) {
77
+ if (nodeIsUniform(node)) {
78
+ for (const arg of node.arguments) {
79
+ if (arg.type === 'Identifier' && namedFunctions.has(arg.name)) {
80
+ names.add(arg.name);
81
+ }
82
+ }
83
+ }
84
+ }
85
+ });
86
+ return names;
87
+ }
88
+
44
89
  function nodeIsVarying(node) {
45
90
  return node && node.type === 'CallExpression'
46
91
  && (
@@ -55,7 +100,64 @@ function nodeIsVarying(node) {
55
100
  )
56
101
  );
57
102
  }
103
+ // Convert static member expressions into dotted paths such as
104
+ // `loopProtect.protect` so loop-protection calls can be matched reliably.
105
+ function getMemberExpressionPath(node) {
106
+ if (node?.type === 'Identifier') return node.name;
107
+
108
+ // Computed properties like `obj[prop]` are not safe to match as fixed paths.
109
+ if (node?.type !== 'MemberExpression' || node.computed) return null;
110
+
111
+ const objectPath = getMemberExpressionPath(node.object);
112
+ const propertyName = node.property?.name;
113
+
114
+ return objectPath && propertyName
115
+ ? `${objectPath}.${propertyName}`
116
+ : null;
117
+ }
118
+
119
+ // Detect calls added by loop protection before Strands tries to transpile them.
120
+ function isLoopProtectionCall(node) {
121
+ if (node?.type !== 'CallExpression') return false;
122
+
123
+ const path = getMemberExpressionPath(node.callee);
124
+
125
+ if (!path) return false;
126
+
127
+ return (
128
+ path === 'loopProtect.protect' ||
129
+ path.endsWith('.loopProtect') ||
130
+ path.endsWith('.loopProtect.protect')
131
+ );
132
+ }
133
+
134
+ // Scan AST for loop-protection injection and throw with `// noprotect` hint.
135
+ function throwIfLoopProtectionInserted(ast) {
136
+ let found = false;
137
+
138
+ ancestor(ast, {
139
+ CallExpression(node) {
140
+ if (isLoopProtectionCall(node)) {
141
+ found = true;
142
+ }
143
+ },
144
+ LogicalExpression(node) {
145
+ // Loop protection may appear as the right side of a short-circuit check.
146
+ if (
147
+ node.right?.type === 'CallExpression' &&
148
+ isLoopProtectionCall(node.right)
149
+ ) {
150
+ found = true;
151
+ }
152
+ }
153
+ });
58
154
 
155
+ if (found) {
156
+ internalError(
157
+ 'loop protection error Loop protection code detected. Add `// noprotect` at the top of your sketch and run again.'
158
+ );
159
+ }
160
+ }
59
161
  // Helper function to check if a statement is a variable declaration with strands control flow init
60
162
  function statementContainsStrandsControlFlow(stmt) {
61
163
  // Check for variable declarations with strands control flow init
@@ -192,9 +294,137 @@ function replaceReferences(node, tempVarMap) {
192
294
  internalReplaceReferences(node);
193
295
  }
194
296
 
297
+ function replaceIdentifierReferences(node, oldName, newName) {
298
+ if (!node || typeof node !== 'object') return node;
299
+
300
+ const replaceInNode = (n) => {
301
+ if (!n || typeof n !== 'object') return n;
302
+ if (n.type === 'Identifier' && n.name === oldName) {
303
+ return { ...n, name: newName };
304
+ }
305
+ const newNode = { ...n };
306
+ for (const key in n) {
307
+ if (n.hasOwnProperty(key) && key !== 'parent') {
308
+ if (Array.isArray(n[key])) {
309
+ newNode[key] = n[key].map(replaceInNode);
310
+ } else if (typeof n[key] === 'object') {
311
+ newNode[key] = replaceInNode(n[key]);
312
+ }
313
+ }
314
+ }
315
+ return newNode;
316
+ };
317
+
318
+ return replaceInNode(node);
319
+ }
320
+
321
+ // Shared handler for both BinaryExpression and LogicalExpression —
322
+ // both follow the same operator-to-method-call transformation pattern.
323
+ function transformBinaryOrLogical(node, state, ancestors) {
324
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
325
+ return;
326
+ }
327
+ const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
328
+ if (unsafeTypes.includes(node.left.type)) {
329
+ node.left = {
330
+ type: 'CallExpression',
331
+ callee: {
332
+ type: 'Identifier',
333
+ name: '__p5.strandsNode',
334
+ },
335
+ arguments: [node.left]
336
+ };
337
+ }
338
+ node.type = 'CallExpression';
339
+ node.callee = {
340
+ type: 'MemberExpression',
341
+ object: node.left,
342
+ property: {
343
+ type: 'Identifier',
344
+ name: replaceBinaryOperator(node.operator),
345
+ },
346
+ };
347
+ node.arguments = [node.right];
348
+ }
349
+
350
+ // Shared helper used by both IfStatement and ForStatement handlers.
351
+ // Adds temp variable copies, replaces references, and appends a return
352
+ // statement to a branch/loop function body.
353
+ // sourcePrefix: the root identifier to read from ('vars' for loops,
354
+ // null for if-branches where we read directly from the outer variable).
355
+ function addCopyingAndReturn(functionBody, varsToReturn, sourcePrefix = null) {
356
+ if (functionBody.type !== 'BlockStatement') return;
357
+
358
+ const tempVarMap = new Map();
359
+ const copyStatements = [];
360
+
361
+ for (const varPath of varsToReturn) {
362
+ const parts = varPath.split('.');
363
+ const tempName = `__copy_${parts.join('_')}_${blockVarCounter++}`;
364
+ tempVarMap.set(varPath, tempName);
365
+
366
+ // If sourcePrefix is set (loop case), read from vars.x.y
367
+ // Otherwise (if-branch case), read directly from x.y
368
+ let sourceExpr = sourcePrefix
369
+ ? { type: 'Identifier', name: sourcePrefix }
370
+ : { type: 'Identifier', name: parts[0] };
371
+
372
+ const pathParts = sourcePrefix ? parts : parts.slice(1);
373
+ for (const part of pathParts) {
374
+ sourceExpr = {
375
+ type: 'MemberExpression',
376
+ object: sourceExpr,
377
+ property: { type: 'Identifier', name: part },
378
+ computed: false
379
+ };
380
+ }
381
+
382
+ copyStatements.push({
383
+ type: 'VariableDeclaration',
384
+ declarations: [{
385
+ type: 'VariableDeclarator',
386
+ id: { type: 'Identifier', name: tempName },
387
+ init: {
388
+ type: 'CallExpression',
389
+ callee: {
390
+ type: 'MemberExpression',
391
+ object: sourceExpr,
392
+ property: { type: 'Identifier', name: 'copy' },
393
+ computed: false
394
+ },
395
+ arguments: []
396
+ }
397
+ }],
398
+ kind: 'let'
399
+ });
400
+ }
401
+
402
+ functionBody.body.forEach(node => replaceReferences(node, tempVarMap));
403
+ functionBody.body.unshift(...copyStatements);
404
+
405
+ const returnObj = {
406
+ type: 'ObjectExpression',
407
+ properties: Array.from(varsToReturn).map(varPath => ({
408
+ type: 'Property',
409
+ key: { type: 'Literal', value: varPath },
410
+ value: { type: 'Identifier', name: tempVarMap.get(varPath) },
411
+ kind: 'init',
412
+ computed: false,
413
+ shorthand: false
414
+ }))
415
+ };
416
+
417
+ functionBody.body.push({
418
+ type: 'ReturnStatement',
419
+ argument: returnObj
420
+ });
421
+ }
422
+
195
423
  const ASTCallbacks = {
196
- UnaryExpression(node, _state, ancestors) {
197
- if (ancestors.some(nodeIsUniform)) { return; }
424
+ UnaryExpression(node, state, ancestors) {
425
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
426
+ return;
427
+ }
198
428
  const unaryFnName = UnarySymbolToName[node.operator];
199
429
  const standardReplacement = (node) => {
200
430
  node.type = 'CallExpression';
@@ -213,7 +443,7 @@ const ASTCallbacks = {
213
443
  ];
214
444
  let isSwizzle = swizzleSets.some(set =>
215
445
  [...property].every(char => set.includes(char))
216
- ) && node.argument.type === 'MemberExpression';
446
+ ) && node.argument.type === 'MemberExpression' && !node.argument.computed;
217
447
  if (isSwizzle) {
218
448
  node.type = 'MemberExpression';
219
449
  node.object = {
@@ -237,8 +467,10 @@ const ASTCallbacks = {
237
467
  delete node.argument;
238
468
  delete node.operator;
239
469
  },
240
- BreakStatement(node, _state, ancestors) {
241
- if (ancestors.some(nodeIsUniform)) { return; }
470
+ BreakStatement(node, state, ancestors) {
471
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
472
+ return;
473
+ }
242
474
  node.callee = {
243
475
  type: 'Identifier',
244
476
  name: '__p5.break'
@@ -246,8 +478,39 @@ const ASTCallbacks = {
246
478
  node.arguments = [];
247
479
  node.type = 'CallExpression';
248
480
  },
249
- VariableDeclarator(node, _state, ancestors) {
250
- if (ancestors.some(nodeIsUniform)) { return; }
481
+ MemberExpression(node, state, ancestors) {
482
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
483
+ return;
484
+ }
485
+ // Skip sets -- these will be converted to .set() method
486
+ // calls at the AssignmentExpression level
487
+ if (
488
+ ancestors.at(-2)?.type === 'AssignmentExpression' &&
489
+ ancestors.at(-2).left === node
490
+ ) {
491
+ return;
492
+ }
493
+ if (node.computed) {
494
+ const callee = node.object;
495
+ const member = node.property;
496
+ node.computed = undefined;
497
+ node.object = undefined;
498
+ node.callee = {
499
+ type: 'MemberExpression',
500
+ object: callee,
501
+ property: {
502
+ type: 'Identifier',
503
+ name: 'get',
504
+ }
505
+ };
506
+ node.arguments = [member];
507
+ node.type = 'CallExpression';
508
+ }
509
+ },
510
+ VariableDeclarator(node, state, ancestors) {
511
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
512
+ return;
513
+ }
251
514
  if (nodeIsUniform(node.init)) {
252
515
  // Only inject the variable name if the first argument isn't already a string
253
516
  if (node.init.arguments.length === 0 ||
@@ -272,16 +535,18 @@ const ASTCallbacks = {
272
535
  value: node.id.name
273
536
  };
274
537
  node.init.arguments.unshift(varyingNameLiteral);
275
- _state.varyings[node.id.name] = varyingNameLiteral;
538
+ state.varyings[node.id.name] = varyingNameLiteral;
276
539
  } else {
277
540
  // Still track it as a varying even if name wasn't injected
278
- _state.varyings[node.id.name] = node.init.arguments[0];
541
+ state.varyings[node.id.name] = node.init.arguments[0];
279
542
  }
280
543
  }
281
544
  },
282
- Identifier(node, _state, ancestors) {
283
- if (ancestors.some(nodeIsUniform)) { return; }
284
- if (_state.varyings[node.name]
545
+ Identifier(node, state, ancestors) {
546
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
547
+ return;
548
+ }
549
+ if (state.varyings[node.name]
285
550
  && !ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node)
286
551
  ) {
287
552
  node.type = 'CallExpression';
@@ -301,8 +566,10 @@ const ASTCallbacks = {
301
566
  },
302
567
  // The callbacks for AssignmentExpression and BinaryExpression handle
303
568
  // operator overloading including +=, *= assignment expressions
304
- ArrayExpression(node, _state, ancestors) {
305
- if (ancestors.some(nodeIsUniform)) { return; }
569
+ ArrayExpression(node, state, ancestors) {
570
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
571
+ return;
572
+ }
306
573
  const original = JSON.parse(JSON.stringify(node));
307
574
  node.type = 'CallExpression';
308
575
  node.callee = {
@@ -311,8 +578,10 @@ const ASTCallbacks = {
311
578
  };
312
579
  node.arguments = [original];
313
580
  },
314
- AssignmentExpression(node, _state, ancestors) {
315
- if (ancestors.some(nodeIsUniform)) { return; }
581
+ AssignmentExpression(node, state, ancestors) {
582
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
583
+ return;
584
+ }
316
585
  const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
317
586
  if (node.operator !== '=') {
318
587
  const methodName = replaceBinaryOperator(node.operator.replace('=',''));
@@ -341,7 +610,7 @@ const ASTCallbacks = {
341
610
  node.right = rightReplacementNode;
342
611
  }
343
612
  // Handle direct varying variable assignment: myVarying = value
344
- if (_state.varyings[node.left.name]) {
613
+ if (state.varyings[node.left.name]) {
345
614
  node.type = 'ExpressionStatement';
346
615
  node.expression = {
347
616
  type: 'CallExpression',
@@ -362,10 +631,31 @@ const ASTCallbacks = {
362
631
  // Handle swizzle assignment to varying variable: myVarying.xyz = value
363
632
  // Note: node.left.object might be worldPos.getValue() due to prior Identifier transformation
364
633
  else if (node.left.type === 'MemberExpression') {
634
+ if (node.left.computed) {
635
+ const source = node.left;
636
+ const value = node.right;
637
+ const callee = source.object;
638
+ const member = source.property;
639
+ node.right = undefined;
640
+ node.left = undefined;
641
+ node.operator = undefined;
642
+ node.callee = {
643
+ type: 'MemberExpression',
644
+ object: callee,
645
+ property: {
646
+ type: 'Identifier',
647
+ name: 'set'
648
+ }
649
+ };
650
+ node.arguments = [member, value];
651
+ node.type = 'CallExpression';
652
+ return;
653
+ }
654
+
365
655
  let varyingName = null;
366
656
 
367
657
  // Check if it's a direct identifier: myVarying.xyz
368
- if (node.left.object.type === 'Identifier' && _state.varyings[node.left.object.name]) {
658
+ if (node.left.object.type === 'Identifier' && state.varyings[node.left.object.name]) {
369
659
  varyingName = node.left.object.name;
370
660
  }
371
661
  // Check if it's a getValue() call: myVarying.getValue().xyz
@@ -373,7 +663,7 @@ const ASTCallbacks = {
373
663
  node.left.object.callee?.type === 'MemberExpression' &&
374
664
  node.left.object.callee.property?.name === 'getValue' &&
375
665
  node.left.object.callee.object?.type === 'Identifier' &&
376
- _state.varyings[node.left.object.callee.object.name]) {
666
+ state.varyings[node.left.object.callee.object.name]) {
377
667
  varyingName = node.left.object.callee.object.name;
378
668
  }
379
669
 
@@ -404,70 +694,30 @@ const ASTCallbacks = {
404
694
  }
405
695
  }
406
696
  },
407
- BinaryExpression(node, _state, ancestors) {
408
- // Don't convert uniform default values to node methods, as
409
- // they should be evaluated at runtime, not compiled.
410
- if (ancestors.some(nodeIsUniform)) { return; }
411
- // If the left hand side of an expression is one of these types,
412
- // we should construct a node from it.
413
- const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
414
- if (unsafeTypes.includes(node.left.type)) {
415
- const leftReplacementNode = {
416
- type: 'CallExpression',
417
- callee: {
418
- type: 'Identifier',
419
- name: '__p5.strandsNode',
420
- },
421
- arguments: [node.left]
422
- };
423
- node.left = leftReplacementNode;
697
+ BinaryExpression: transformBinaryOrLogical,
698
+ LogicalExpression: transformBinaryOrLogical,
699
+
700
+
701
+ ConditionalExpression(node, state, ancestors) {
702
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
703
+ return;
424
704
  }
425
- // Replace the binary operator with a call expression
426
- // in other words a call to BaseNode.mult(), .div() etc.
705
+ // Transform condition ? consequent : alternate
706
+ // into __p5.strandsTernary(condition, consequent, alternate)
707
+ const test = node.test;
708
+ const consequent = node.consequent;
709
+ const alternate = node.alternate;
427
710
  node.type = 'CallExpression';
428
- node.callee = {
429
- type: 'MemberExpression',
430
- object: node.left,
431
- property: {
432
- type: 'Identifier',
433
- name: replaceBinaryOperator(node.operator),
434
- },
435
- };
436
- node.arguments = [node.right];
711
+ node.callee = { type: 'Identifier', name: '__p5.strandsTernary' };
712
+ node.arguments = [test, consequent, alternate];
713
+ delete node.test;
714
+ delete node.consequent;
715
+ delete node.alternate;
437
716
  },
438
- LogicalExpression(node, _state, ancestors) {
439
- // Don't convert uniform default values to node methods, as
440
- // they should be evaluated at runtime, not compiled.
441
- if (ancestors.some(nodeIsUniform)) { return; }
442
- // If the left hand side of an expression is one of these types,
443
- // we should construct a node from it.
444
- const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
445
- if (unsafeTypes.includes(node.left.type)) {
446
- const leftReplacementNode = {
447
- type: 'CallExpression',
448
- callee: {
449
- type: 'Identifier',
450
- name: '__p5.strandsNode',
451
- },
452
- arguments: [node.left]
453
- };
454
- node.left = leftReplacementNode;
717
+ IfStatement(node, state, ancestors) {
718
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
719
+ return;
455
720
  }
456
- // Replace the logical operator with a call expression
457
- // in other words a call to BaseNode.or(), .and() etc.
458
- node.type = 'CallExpression';
459
- node.callee = {
460
- type: 'MemberExpression',
461
- object: node.left,
462
- property: {
463
- type: 'Identifier',
464
- name: replaceBinaryOperator(node.operator),
465
- },
466
- };
467
- node.arguments = [node.right];
468
- },
469
- IfStatement(node, _state, ancestors) {
470
- if (ancestors.some(nodeIsUniform)) { return; }
471
721
  // Transform if statement into strandsIf() call
472
722
  // The condition is evaluated directly, not wrapped in a function
473
723
  const condition = node.test;
@@ -571,70 +821,6 @@ const ASTCallbacks = {
571
821
  analyzeBranch(thenFunction.body);
572
822
  analyzeBranch(elseFunction.body);
573
823
  if (assignedVars.size > 0) {
574
- // Add copying, reference replacement, and return statements to branch functions
575
- const addCopyingAndReturn = (functionBody, varsToReturn) => {
576
- if (functionBody.type === 'BlockStatement') {
577
- // Create temporary variables and copy statements
578
- const tempVarMap = new Map(); // property path -> temp name
579
- const copyStatements = [];
580
- for (const varPath of varsToReturn) {
581
- const parts = varPath.split('.');
582
- const tempName = `__copy_${parts.join('_')}_${blockVarCounter++}`;
583
- tempVarMap.set(varPath, tempName);
584
-
585
- // Build the member expression for the property path
586
- let sourceExpr = { type: 'Identifier', name: parts[0] };
587
- for (let i = 1; i < parts.length; i++) {
588
- sourceExpr = {
589
- type: 'MemberExpression',
590
- object: sourceExpr,
591
- property: { type: 'Identifier', name: parts[i] },
592
- computed: false
593
- };
594
- }
595
-
596
- // let tempName = propertyPath.copy()
597
- copyStatements.push({
598
- type: 'VariableDeclaration',
599
- declarations: [{
600
- type: 'VariableDeclarator',
601
- id: { type: 'Identifier', name: tempName },
602
- init: {
603
- type: 'CallExpression',
604
- callee: {
605
- type: 'MemberExpression',
606
- object: sourceExpr,
607
- property: { type: 'Identifier', name: 'copy' },
608
- computed: false
609
- },
610
- arguments: []
611
- }
612
- }],
613
- kind: 'let'
614
- });
615
- }
616
- // Apply reference replacement to all statements
617
- functionBody.body.forEach(node => replaceReferences(node, tempVarMap));
618
- // Insert copy statements at the beginning
619
- functionBody.body.unshift(...copyStatements);
620
- // Add return statement with flat object using property paths as keys
621
- const returnObj = {
622
- type: 'ObjectExpression',
623
- properties: Array.from(varsToReturn).map(varPath => ({
624
- type: 'Property',
625
- key: { type: 'Literal', value: varPath },
626
- value: { type: 'Identifier', name: tempVarMap.get(varPath) },
627
- kind: 'init',
628
- computed: false,
629
- shorthand: false
630
- }))
631
- };
632
- functionBody.body.push({
633
- type: 'ReturnStatement',
634
- argument: returnObj
635
- });
636
- }
637
- };
638
824
  addCopyingAndReturn(thenFunction.body, assignedVars);
639
825
  addCopyingAndReturn(elseFunction.body, assignedVars);
640
826
  // Create a block variable to capture the return value
@@ -735,8 +921,10 @@ const ASTCallbacks = {
735
921
  delete node.consequent;
736
922
  delete node.alternate;
737
923
  },
738
- UpdateExpression(node, _state, ancestors) {
739
- if (ancestors.some(nodeIsUniform)) { return; }
924
+ UpdateExpression(node, state, ancestors) {
925
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
926
+ return;
927
+ }
740
928
 
741
929
  // Transform ++var, var++, --var, var-- into assignment expressions
742
930
  let operator;
@@ -767,11 +955,13 @@ const ASTCallbacks = {
767
955
  // Replace the update expression with the assignment expression
768
956
  Object.assign(node, assignmentExpr);
769
957
  delete node.prefix;
770
- this.BinaryExpression(node.right, _state, [...ancestors, node]);
771
- this.AssignmentExpression(node, _state, ancestors);
958
+ ASTCallbacks.BinaryExpression(node.right, state, [...ancestors, node]);
959
+ ASTCallbacks.AssignmentExpression(node, state, ancestors);
772
960
  },
773
- ForStatement(node, _state, ancestors) {
774
- if (ancestors.some(nodeIsUniform)) { return; }
961
+ ForStatement(node, state, ancestors) {
962
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
963
+ return;
964
+ }
775
965
 
776
966
  // Transform for statement into strandsFor() call
777
967
  // for (init; test; update) body -> strandsFor(initCb, conditionCb, updateCb, bodyCb, initialVars)
@@ -823,7 +1013,7 @@ const ASTCallbacks = {
823
1013
  // Replace loop variable references with the parameter
824
1014
  if (node.init?.type === 'VariableDeclaration') {
825
1015
  const loopVarName = node.init.declarations[0].id.name;
826
- conditionBody = this.replaceIdentifierReferences(conditionBody, loopVarName, uniqueLoopVar);
1016
+ conditionBody = replaceIdentifierReferences(conditionBody, loopVarName, uniqueLoopVar);
827
1017
  }
828
1018
  const conditionAst = { body: [{ type: 'ExpressionStatement', expression: conditionBody }] };
829
1019
  conditionBody = conditionAst.body[0].expression;
@@ -841,7 +1031,7 @@ const ASTCallbacks = {
841
1031
  // Replace loop variable references with the parameter
842
1032
  if (node.init?.type === 'VariableDeclaration') {
843
1033
  const loopVarName = node.init.declarations[0].id.name;
844
- updateExpr = this.replaceIdentifierReferences(updateExpr, loopVarName, uniqueLoopVar);
1034
+ updateExpr = replaceIdentifierReferences(updateExpr, loopVarName, uniqueLoopVar);
845
1035
  }
846
1036
  const updateAst = { body: [{ type: 'ExpressionStatement', expression: updateExpr }] };
847
1037
  updateExpr = updateAst.body[0].expression;
@@ -880,7 +1070,7 @@ const ASTCallbacks = {
880
1070
  // Replace loop variable references in the body
881
1071
  if (node.init?.type === 'VariableDeclaration') {
882
1072
  const loopVarName = node.init.declarations[0].id.name;
883
- bodyBlock = this.replaceIdentifierReferences(bodyBlock, loopVarName, uniqueLoopVar);
1073
+ bodyBlock = replaceIdentifierReferences(bodyBlock, loopVarName, uniqueLoopVar);
884
1074
  }
885
1075
 
886
1076
  const bodyFunction = {
@@ -935,73 +1125,8 @@ const ASTCallbacks = {
935
1125
  });
936
1126
 
937
1127
  if (assignedVars.size > 0) {
938
- // Add copying, reference replacement, and return statements similar to if statements
939
- const addCopyingAndReturn = (functionBody, varsToReturn) => {
940
- if (functionBody.type === 'BlockStatement') {
941
- const tempVarMap = new Map();
942
- const copyStatements = [];
943
-
944
- for (const varPath of varsToReturn) {
945
- const parts = varPath.split('.');
946
- const tempName = `__copy_${parts.join('_')}_${blockVarCounter++}`;
947
- tempVarMap.set(varPath, tempName);
948
-
949
- // Build the member expression for vars.propertyPath
950
- // e.g., vars.inputs.color or vars.x
951
- let sourceExpr = { type: 'Identifier', name: 'vars' };
952
- for (const part of parts) {
953
- sourceExpr = {
954
- type: 'MemberExpression',
955
- object: sourceExpr,
956
- property: { type: 'Identifier', name: part },
957
- computed: false
958
- };
959
- }
960
-
961
- copyStatements.push({
962
- type: 'VariableDeclaration',
963
- declarations: [{
964
- type: 'VariableDeclarator',
965
- id: { type: 'Identifier', name: tempName },
966
- init: {
967
- type: 'CallExpression',
968
- callee: {
969
- type: 'MemberExpression',
970
- object: sourceExpr,
971
- property: { type: 'Identifier', name: 'copy' },
972
- computed: false
973
- },
974
- arguments: []
975
- }
976
- }],
977
- kind: 'let'
978
- });
979
- }
980
-
981
- functionBody.body.forEach(node => replaceReferences(node, tempVarMap));
982
- functionBody.body.unshift(...copyStatements);
983
-
984
- // Add return statement with flat object using property paths as keys
985
- const returnObj = {
986
- type: 'ObjectExpression',
987
- properties: Array.from(varsToReturn).map(varPath => ({
988
- type: 'Property',
989
- key: { type: 'Literal', value: varPath },
990
- value: { type: 'Identifier', name: tempVarMap.get(varPath) },
991
- kind: 'init',
992
- computed: false,
993
- shorthand: false
994
- }))
995
- };
996
-
997
- functionBody.body.push({
998
- type: 'ReturnStatement',
999
- argument: returnObj
1000
- });
1001
- }
1002
- };
1003
1128
 
1004
- addCopyingAndReturn(bodyFunction.body, assignedVars);
1129
+ addCopyingAndReturn(bodyFunction.body, assignedVars, 'vars');
1005
1130
 
1006
1131
  // Create block variable and assignments similar to if statements
1007
1132
  const blockVar = `__block_${blockVarCounter++}`;
@@ -1116,33 +1241,8 @@ const ASTCallbacks = {
1116
1241
  delete node.update;
1117
1242
  },
1118
1243
 
1119
- // Helper method to replace identifier references in AST nodes
1120
- replaceIdentifierReferences(node, oldName, newName) {
1121
- if (!node || typeof node !== 'object') return node;
1122
-
1123
- const replaceInNode = (n) => {
1124
- if (!n || typeof n !== 'object') return n;
1125
-
1126
- if (n.type === 'Identifier' && n.name === oldName) {
1127
- return { ...n, name: newName };
1128
- }
1129
-
1130
- // Create a copy and recursively process properties
1131
- const newNode = { ...n };
1132
- for (const key in n) {
1133
- if (n.hasOwnProperty(key) && key !== 'parent') {
1134
- if (Array.isArray(n[key])) {
1135
- newNode[key] = n[key].map(replaceInNode);
1136
- } else if (typeof n[key] === 'object') {
1137
- newNode[key] = replaceInNode(n[key]);
1138
- }
1139
- }
1140
- }
1141
- return newNode;
1142
- };
1143
-
1144
- return replaceInNode(node);
1145
- }
1244
+
1245
+
1146
1246
  };
1147
1247
 
1148
1248
  // Helper function to check if a function body contains return statements in control flow
@@ -1477,22 +1577,31 @@ function transformFunctionSetCalls(functionNode) {
1477
1577
  }
1478
1578
 
1479
1579
  // Main transformation pass: find and transform functions with .set() calls in control flow
1480
- function transformSetCallsInControlFlow(ast) {
1580
+ function transformSetCallsInControlFlow(ast, names) {
1481
1581
  const functionsToTransform = [];
1482
1582
 
1483
1583
  // Collect functions that have .set() calls in control flow
1484
1584
  const collectFunctions = {
1485
1585
  ArrowFunctionExpression(node, ancestors) {
1586
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, names))) {
1587
+ return;
1588
+ }
1486
1589
  if (functionHasSetInControlFlow(node)) {
1487
1590
  functionsToTransform.push(node);
1488
1591
  }
1489
1592
  },
1490
1593
  FunctionExpression(node, ancestors) {
1594
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, names))) {
1595
+ return;
1596
+ }
1491
1597
  if (functionHasSetInControlFlow(node)) {
1492
1598
  functionsToTransform.push(node);
1493
1599
  }
1494
1600
  },
1495
1601
  FunctionDeclaration(node, ancestors) {
1602
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, names))) {
1603
+ return;
1604
+ }
1496
1605
  if (functionHasSetInControlFlow(node)) {
1497
1606
  functionsToTransform.push(node);
1498
1607
  }
@@ -1508,12 +1617,15 @@ function transformSetCallsInControlFlow(ast) {
1508
1617
  }
1509
1618
 
1510
1619
  // Main transformation pass: find and transform helper functions with early returns
1511
- function transformHelperFunctionEarlyReturns(ast) {
1620
+ function transformHelperFunctionEarlyReturns(ast, names) {
1512
1621
  const helperFunctionsToTransform = [];
1513
1622
 
1514
1623
  // Collect helper functions that need transformation
1515
1624
  const collectHelperFunctions = {
1516
1625
  VariableDeclarator(node, ancestors) {
1626
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, names))) {
1627
+ return;
1628
+ }
1517
1629
  const init = node.init;
1518
1630
  if (init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) {
1519
1631
  if (functionHasEarlyReturns(init)) {
@@ -1522,6 +1634,9 @@ function transformHelperFunctionEarlyReturns(ast) {
1522
1634
  }
1523
1635
  },
1524
1636
  FunctionDeclaration(node, ancestors) {
1637
+ if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, names))) {
1638
+ return;
1639
+ }
1525
1640
  if (functionHasEarlyReturns(node)) {
1526
1641
  helperFunctionsToTransform.push(node);
1527
1642
  }
@@ -1541,6 +1656,33 @@ function transformHelperFunctionEarlyReturns(ast) {
1541
1656
  }
1542
1657
  }
1543
1658
 
1659
+ /**
1660
+ * Transpiles a p5.strands callback into executable JavaScript by applying
1661
+ * a multi-pass AST transformation pipeline.
1662
+ *
1663
+ * Pipeline stages:
1664
+ *
1665
+ * 1. Collect uniform callback names
1666
+ * - Identifies functions passed into uniform() so they are excluded from transformation
1667
+ *
1668
+ * 2. transformSetCallsInControlFlow
1669
+ * - Rewrites `.set()` calls inside control flow into intermediate variable assignments
1670
+ *
1671
+ * 3. Non-control-flow transformations
1672
+ * - Applies ASTCallbacks to transform expressions, assignments, etc.
1673
+ * - Skips IfStatement and ForStatement (handled later)
1674
+ *
1675
+ * 4. transformHelperFunctionEarlyReturns
1676
+ * - Converts early returns in helper functions into a single return value pattern
1677
+ *
1678
+ * 5. Control flow transformation (post-order)
1679
+ * - Transforms IfStatement → __p5.strandsIf
1680
+ * - Transforms ForStatement → __p5.strandsFor
1681
+ * - Handles variable propagation across branches/loops
1682
+ *
1683
+ * This staged approach ensures correct ordering and avoids transformation conflicts.
1684
+ */
1685
+
1544
1686
  function transpileStrandsToJS(p5, sourceString, srcLocations, scope) {
1545
1687
  // Reset counters at the start of each transpilation
1546
1688
  blockVarCounter = 0;
@@ -1551,20 +1693,43 @@ function transpileStrandsToJS(p5, sourceString, srcLocations, scope) {
1551
1693
  locations: srcLocations
1552
1694
  });
1553
1695
 
1696
+ throwIfLoopProtectionInserted(ast);
1697
+
1698
+ // Pre-pass: collect names of functions passed by reference as uniform callbacks
1699
+ const uniformCallbackNames = collectUniformCallbackNames(ast);
1700
+
1554
1701
  // First pass: transform .set() calls in control flow to use intermediate variables
1555
- transformSetCallsInControlFlow(ast);
1702
+ transformSetCallsInControlFlow(ast, uniformCallbackNames);
1556
1703
 
1557
1704
  // Second pass: transform everything except if/for statements using normal ancestor traversal
1558
1705
  const nonControlFlowCallbacks = { ...ASTCallbacks };
1559
1706
  delete nonControlFlowCallbacks.IfStatement;
1560
1707
  delete nonControlFlowCallbacks.ForStatement;
1561
- ancestor(ast, nonControlFlowCallbacks, undefined, { varyings: {} });
1708
+ ancestor(ast, nonControlFlowCallbacks, undefined, { varyings: {}, uniformCallbackNames });
1562
1709
 
1563
1710
  // Third pass: transform helper functions with early returns to use __returnValue pattern
1564
- transformHelperFunctionEarlyReturns(ast);
1711
+ transformHelperFunctionEarlyReturns(ast, uniformCallbackNames);
1565
1712
 
1566
1713
  // Fourth pass: transform if/for statements in post-order using recursive traversal
1567
1714
  const postOrderControlFlowTransform = {
1715
+ CallExpression(node, state, c) {
1716
+ if (nodeIsUniform(node)) { return; }
1717
+ if (node.callee) c(node.callee, state);
1718
+ for (const arg of node.arguments) c(arg, state);
1719
+ },
1720
+ FunctionDeclaration(node, state, c) {
1721
+ if (state.uniformCallbackNames?.has(node.id?.name)) return;
1722
+ if (node.body) c(node.body, state);
1723
+ },
1724
+ VariableDeclarator(node, state, c) {
1725
+ if (
1726
+ state.uniformCallbackNames?.has(node.id?.name) &&
1727
+ (node.init?.type === 'FunctionExpression' || node.init?.type === 'ArrowFunctionExpression')
1728
+ ) {
1729
+ return;
1730
+ }
1731
+ if (node.init) c(node.init, state);
1732
+ },
1568
1733
  IfStatement(node, state, c) {
1569
1734
  state.inControlFlow++;
1570
1735
  // First recursively process children
@@ -1601,7 +1766,7 @@ function transpileStrandsToJS(p5, sourceString, srcLocations, scope) {
1601
1766
  delete node.argument;
1602
1767
  }
1603
1768
  };
1604
- recursive(ast, { varyings: {}, inControlFlow: 0 }, postOrderControlFlowTransform);
1769
+ recursive(ast, { varyings: {}, inControlFlow: 0, uniformCallbackNames }, postOrderControlFlowTransform);
1605
1770
  const transpiledSource = escodegen.generate(ast);
1606
1771
  const scopeKeys = Object.keys(scope);
1607
1772
  const match = /\(?\s*(?:function)?\s*\w*\s*\(([^)]*)\)\s*(?:=>)?\s*{((?:.|\n)*)}\s*;?\s*\)?/