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) {
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, {
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