From 645ccd1129b88c33ec98879f9e094b63b7afb24f Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Thu, 21 May 2026 16:43:29 +0530 Subject: [PATCH] Add paletteLerp for p5.strands (closes #8751) --- src/strands/strands_api.js | 38 +++++++++++ src/strands/strands_builtins.js | 2 + src/strands/strands_transpiler.js | 107 ++++++++++++++++++++++++++++++ src/webgl/strands_glslBackend.js | 1 + 4 files changed, 148 insertions(+) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 69bb2f4179..8b83ff0201 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -400,6 +400,44 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }); return createStrandsNode(id, dimension, strandsContext); }); + augmentFn(fn, p5, 'paletteLerp', function(colorsNode, positionsNode, tNode) { + if (!strandsContext.active) return; + + const n = colorsNode.length; + + // Wrap raw values into StrandsNodes + const colors = colorsNode.map(c => p5.strandsNode(c)); + const positions = positionsNode.map(p => p5.strandsNode(p)); + const t = p5.strandsNode(tNode); + + // Helper: mix(a, b, clamp((t - pa) / (pb - pa), 0, 1)) + function segmentLerp(ca, cb, pa, pb) { + const zero = p5.strandsNode(0.0); + const one = p5.strandsNode(1.0); + const num = t.sub(pa); + const den = pb.sub(pa); + const localT = num.div(den).clamp(zero, one); + return buildTernary( + strandsContext, + pa.equalTo(pb), // guard: pa == pb → return midpoint + ca.mix(cb, p5.strandsNode(0.5)), + ca.mix(cb, localT) + ); + } + + // Build nested ternary chain from right to left: + // t >= p[last] ? c[last] : (t < p[n-1] ? seg(n-2,n-1) : (...)) + let result = colors[n - 1]; + for (let i = n - 2; i >= 0; i--) { + const seg = segmentLerp(colors[i], colors[i + 1], positions[i], positions[i + 1]); + result = buildTernary(strandsContext, t.lessThan(positions[i + 1]), seg, result); + } + // Clamp edges + result = buildTernary(strandsContext, t.greaterEqual(positions[n - 1]), colors[n - 1], result); + result = buildTernary(strandsContext, t.lessEqual(positions[0]), colors[0], result); + + return result; + }); strandsContext._randomSeed = null; diff --git a/src/strands/strands_builtins.js b/src/strands/strands_builtins.js index d6080ea428..f1b21b00d6 100644 --- a/src/strands/strands_builtins.js +++ b/src/strands/strands_builtins.js @@ -110,3 +110,5 @@ const builtInGLSLFunctions = { export const strandsBuiltinFunctions = { ...builtInGLSLFunctions, } + + diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index bf33907338..6108c6ee25 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -3,6 +3,16 @@ import { ancestor, recursive } from 'acorn-walk'; import escodegen from 'escodegen'; import { UnarySymbolToName } from './ir_types'; import * as FES from './strands_FES'; + +// Registry of strands functions that take raw array literals as arguments. +// Maps functionName → Set of argument indices that should NOT be +// converted to vectors by the ArrayExpression visitor. +// This generalizes the paletteLerp special-case so any future function +// taking array parameters can register here without modifying ArrayExpression. +const ARRAY_ARG_FUNCTIONS = { + paletteLerp: new Set([0]), // argument 0 is the [[color,pos],...] array +}; + let blockVarCounter = 0; let loopVarCounter = 0; function replaceBinaryOperator(codeSource) { @@ -563,12 +573,85 @@ const ASTCallbacks = { node.arguments = []; } }, + + // Rewrite paletteLerp([[color,pos],...], t) → __p5.paletteLerp([c,...], [p,...], t) + // Must run before ArrayExpression so the child arrays carry _isPaletteLerpArg + // and are not wrapped in strandsNode (which would mis-type them as vectors). + + CallExpression(node, state, ancestors) { + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { + return; + } + if (node.callee?.type !== 'Identifier' || node.callee?.name !== 'paletteLerp') { + return; + } + const args = node.arguments; + if (args.length !== 2) { + throw new Error( + `paletteLerp() requires 2 arguments: (colorStops[], t) — got ${args.length}.\n` + + `Usage: paletteLerp([[color(r,g,b), pos], ...], t)` + ); + } + const [stopsArg, tArg] = args; + if (stopsArg.type !== 'ArrayExpression') { + throw new Error( + `paletteLerp() first argument must be an array literal: [[color(...), pos], ...]` + ); + } + const stops = stopsArg.elements; + if (stops.length < 2 || stops.length > 8) { + throw new Error( + `paletteLerp() requires 2–8 color stops, got ${stops.length}.` + ); + } + for (let i = 0; i < stops.length; i++) { + if (stops[i].type !== 'ArrayExpression' || stops[i].elements.length !== 2) { + throw new Error( + `paletteLerp() stop ${i} must be a 2-element array: [color(...), position]` + ); + } + } + // Split pairs into two parallel arrays + const colorsArr = { + type: 'ArrayExpression', + elements: stops.map(s => s.elements[0]), + _isPaletteLerpArg: true, + }; + const positionsArr = { + type: 'ArrayExpression', + elements: stops.map(s => s.elements[1]), + _isPaletteLerpArg: true, + }; + // Rewrite in-place to __p5.paletteLerp(colors, positions, t) + node.callee = { type: 'Identifier', name: '__p5.paletteLerp' }; + node.arguments = [colorsArr, positionsArr, tArg]; + }, + + // The callbacks for AssignmentExpression and BinaryExpression handle // operator overloading including +=, *= assignment expressions + + ArrayExpression(node, state, ancestors) { if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) { return; } + // Don't wrap arrays that are arguments to functions expecting raw arrays. + // Walk ancestors to find the nearest CallExpression and check the registry. + for (let i = ancestors.length - 1; i >= 0; i--) { + const a = ancestors[i]; + if (a.type === 'CallExpression') { + const name = a.callee?.name; + if (name && ARRAY_ARG_FUNCTIONS[name]) { + const argIndex = a.arguments.indexOf(node); + if (argIndex !== -1 && ARRAY_ARG_FUNCTIONS[name].has(argIndex)) { + return; + } + } + break; // only check nearest CallExpression + } + } + const original = JSON.parse(JSON.stringify(node)); node.type = 'CallExpression'; node.callee = { @@ -1700,6 +1783,30 @@ export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { // First pass: transform .set() calls in control flow to use intermediate variables transformSetCallsInControlFlow(ast, uniformCallbackNames); + // paletteLerp pre-pass: must run before the main pass so ArrayExpression + // doesn't wrap [[color,pos],...] as a vector before we can split the pairs. + ancestor(ast, { + CallExpression(node, state, ancestors) { + if (node.callee?.type !== 'Identifier' || node.callee?.name !== 'paletteLerp') return; + if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) return; + const [stopsArg, tArg] = node.arguments; + if (node.arguments.length !== 2) throw new Error(`paletteLerp() requires 2 arguments: ([[color,pos],...], t)`); + if (stopsArg.type !== 'ArrayExpression') throw new Error(`paletteLerp() first argument must be an array literal`); + const stops = stopsArg.elements; + if (stops.length < 2 || stops.length > 8) throw new Error(`paletteLerp() requires 2–8 color stops, got ${stops.length}`); + for (let i = 0; i < stops.length; i++) { + if (stops[i].type !== 'ArrayExpression' || stops[i].elements.length !== 2) + throw new Error(`paletteLerp() stop ${i} must be [color(...), position]`); + } + node.callee = { type: 'Identifier', name: '__p5.paletteLerp' }; + node.arguments = [ + { type: 'ArrayExpression', elements: stops.map(s => s.elements[0]) }, + { type: 'ArrayExpression', elements: stops.map(s => s.elements[1]) }, + tArg + ]; + } + }, undefined, { uniformCallbackNames }); + // Second pass: transform everything except if/for statements using normal ancestor traversal const nonControlFlowCallbacks = { ...ASTCallbacks }; delete nonControlFlowCallbacks.IfStatement; diff --git a/src/webgl/strands_glslBackend.js b/src/webgl/strands_glslBackend.js index 9ec04b4fae..c64f364802 100644 --- a/src/webgl/strands_glslBackend.js +++ b/src/webgl/strands_glslBackend.js @@ -181,6 +181,7 @@ export const glslBackend = { getRandomVertexShaderSnippet() { return randomVertGLSL; }, + getTypeName(baseType, dimension) { const primitiveTypeName = TypeNames[baseType + dimension] if (!primitiveTypeName) {