diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 2ebbde4168..2a17d00444 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -353,6 +353,16 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }; } } + + StrandsNode.prototype.pow = function (...right) { + const ctx = this.strandsContext || strandsContext; + const { id, dimension } = build.functionCallNode( + ctx, + 'pow', + [this, ...right] + ); + return createStrandsNode(id, dimension, ctx); + }; ////////////////////////////////////////////// // Unique Functions ////////////////////////////////////////////// diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index 8269af368f..ec85037971 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -22,6 +22,8 @@ function replaceBinaryOperator(codeSource) { return 'div'; case '%': return 'mod'; + case '**': + return 'pow'; case '==': case '===': return 'equalTo'; @@ -40,8 +42,11 @@ function replaceBinaryOperator(codeSource) { return 'and'; case '||': return 'or'; - // TODO: handle ** --> pow, but make it stay pow in - // GLSL instead of turning it back into ** + default: + FES.userError( + 'operator error', + `The operator '${codeSource}' is not supported in shader functions.` + ); } } function nodeIsUniform(ancestor) { @@ -49,10 +54,10 @@ function nodeIsUniform(ancestor) { ancestor && ancestor.type === 'CallExpression' && // Global mode ((ancestor.callee?.type === 'Identifier' && - ancestor.callee?.name.startsWith('uniform')) || + ancestor.callee?.name?.startsWith('uniform')) || // Instance mode (ancestor.callee?.type === 'MemberExpression' && - ancestor.callee?.property.name.startsWith('uniform'))) + ancestor.callee?.property?.name?.startsWith('uniform'))) ); } @@ -109,12 +114,12 @@ function nodeIsVarying(node) { node && node.type === 'CallExpression' && // Global mode ((node.callee?.type === 'Identifier' && - (node.callee?.name.startsWith('varying') || - node.callee?.name.startsWith('shared'))) || + (node.callee?.name?.startsWith('varying') || + node.callee?.name?.startsWith('shared'))) || // Instance mode (node.callee?.type === 'MemberExpression' && - (node.callee?.property.name.startsWith('varying') || - node.callee?.property.name.startsWith('shared')))) + (node.callee?.property?.name?.startsWith('varying') || + node.callee?.property?.name?.startsWith('shared')))) ); } // Convert static member expressions into dotted paths such as diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index ec3c0c8d28..65597837ac 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -3007,6 +3007,54 @@ suite('p5.Shader', function () { ); }); + test('exponentiation operator ** in filter shader', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseFilterShader().modify( + () => { + myp5.getColor(() => { + const base = 0.5; + const result = base ** 2; + return [result, 0, 0, 1]; + }); + }, + { myp5 } + ); + + myp5.filter(testShader); + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 64, 5); // 0.25 * 255 ≈ 64 + assert.approximately(pixelColor[1], 0, 5); + assert.approximately(pixelColor[2], 0, 5); + }); + + test('exponentiation operator ** inline and with vectors and **=', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + const testShader = myp5.baseMaterialShader().modify( + () => { + myp5.getPixelInputs(inputs => { + let v = 0.5; + v **= 2; + const vec = [0.5, 0.5]; + const vecPow = vec ** 2; + inputs.color = [v, vecPow.x, 0.5 ** 2, 1.0]; + return inputs; + }); + }, + { myp5 } + ); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 64, 5); + assert.approximately(pixelColor[1], 64, 5); + assert.approximately(pixelColor[2], 64, 5); + }); + test('handle complex filter shader with for loop and vector operations', () => { myp5.createCanvas(50, 50, myp5.WEBGL); @@ -4376,5 +4424,43 @@ suite('p5.Shader', function () { assert.include(errMsg, 'begin()'); assert.include(errMsg, 'end()'); }); + + test('unsupported binary operator throws a clear strands operator error', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + mockUserError.mockClear(); + try { + myp5.baseMaterialShader().modify( + () => { + myp5.getPixelInputs(inputs => { + const a = 1; + const b = 2; + /* oxlint-disable-next-line no-bitwise */ + const c = a & b; + inputs.color = [c, 0, 0, 1]; + return inputs; + }); + }, + { myp5 } + ); + } catch { + /* expected */ + } + + assert.isAbove( + mockUserError.mock.calls.length, + 0, + 'FES.userError should have been called' + ); + const opCall = mockUserError.mock.calls.find( + call => call[0] === 'operator error' + ); + assert.isDefined(opCall, 'operator error should have been called'); + const errMsg = opCall[1]; + assert.include( + errMsg, + "The operator '&' is not supported in shader functions" + ); + }); }); });