Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/strands/strands_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 2 additions & 0 deletions src/strands/strands_builtins.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,5 @@ const builtInGLSLFunctions = {
export const strandsBuiltinFunctions = {
...builtInGLSLFunctions,
}


107 changes: 107 additions & 0 deletions src/strands/strands_transpiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to generalize this too? Ideally we don't need anything specific to one function in the transpiler, and it's all generalized enough that we could add to it for another function if needed. I see that currently this is used to split the array of pairs into two separate arrays. Is this needed if we don't do that?

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 = {
Expand Down Expand Up @@ -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, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need this with the ARRAY_ARG_FUNCTIONS check?

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;
Expand Down
1 change: 1 addition & 0 deletions src/webgl/strands_glslBackend.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export const glslBackend = {
getRandomVertexShaderSnippet() {
return randomVertGLSL;
},

getTypeName(baseType, dimension) {
const primitiveTypeName = TypeNames[baseType + dimension]
if (!primitiveTypeName) {
Expand Down