Skip to content

Add paletteLerp for p5.strands (closes #8751)#8817

Open
LalitNarayanYadav wants to merge 1 commit into
processing:dev-2.0from
LalitNarayanYadav:feat/strands-palette-lerp
Open

Add paletteLerp for p5.strands (closes #8751)#8817
LalitNarayanYadav wants to merge 1 commit into
processing:dev-2.0from
LalitNarayanYadav:feat/strands-palette-lerp

Conversation

@LalitNarayanYadav
Copy link
Copy Markdown
Contributor

Resolves #8751

Changes

Implements paletteLerp for p5.strands, giving GPU-side feature parity with the CPU paletteLerp.

strands_transpiler.js

Adds a pre-pass that rewrites paletteLerp([[color, pos], ...], t) into parallel color/position arrays before the main ArrayExpression pass fires. This is necessary because acorn-walk visits children before parents — the outer array would be mis-typed as a vector before the paletteLerp handler could split the pairs. A _isPaletteLerpArg flag prevents the resulting arrays from being wrapped in strandsNode.

strands_api.js

Registers paletteLerp as a dual-mode runtime function. When inside a strands context, builds an IR FUNCTION_CALL node and injects the GLSL helper functions into the shader preamble once via getPaletteLerpGLSL(). CPU fallback is preserved.

strands_glslBackend.js

Adds getPaletteLerpGLSL() which returns overloaded GLSL helpers for 2–8 color stops using typed array constructors (vec4[N], float[N]). Overloads are required because GLSL ES 3.0 has no variadic functions.

strands_builtins.js

Minor cleanup.

Usage

let s = buildFilterShader(() => {
  filterColor.begin();
  filterColor.set(paletteLerp(
    [
      [color(255, 0, 0), 0.0],
      [color(0, 255, 0), 0.5],
      [color(0, 0, 255), 1.0],
    ],
    filterColor.texCoord.x
  ));
  filterColor.end();
});

function draw() {
  background(220);
  rect(-200, -200, 400, 400);
  filter(s);
}

This matches the ergonomics of the CPU paletteLerp:

// CPU equivalent
paletteLerp(
  [color(255, 0, 0), color(0, 255, 0), color(0, 0, 255)],
  [0.0, 0.5, 1.0],
  map(mouseX, 0, width, 0, 1)
);

Notes

  • Supports 2–8 color stops (covers the vast majority of gradient use cases; more stops can be chained)
  • WGSL path not included — can be a follow-up
  • All 157 existing shader tests pass with no regressions

Screenshots

No visual rendering screenshot included — browser cache issues prevented local verification. The full pipeline (transpiler → IR → GLSL emission) was verified by code review and the existing test suite passes cleanly.

PR Checklist

  • npm run lint passes
  • Inline reference is included / updated — JSDoc to be added in a follow-up
  • Unit tests are included / updated — no strands unit test infrastructure exists yet; tests to be added once the framework is in place

@perminder-17 perminder-17 self-requested a review May 21, 2026 12:24
Copy link
Copy Markdown
Contributor

@davepagurek davepagurek left a comment

Choose a reason for hiding this comment

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

Thanks for taking this on! I had a few questions at a high level around the approach. We should probably also add some visual tests in WebGL and WebGPU mode too.

Comment thread src/strands/strands_transpiler.js Outdated
`Array literals in shader functions are transpiled to vectors and must have 2-4 elements (got ${node.elements.length}).`
);
// Don't wrap paletteLerp's pre-split color/position arrays as vectors
if (node._isPaletteLerpArg) {
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.

We talked a bit in #8751 (comment) about a more generalized way of doing this that wouldn't be specific to paletteLerp, is that something we could add?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's a good point. I'll implement the parameter type registry approach we discussed. The plan is to add a strandsParamTypes map (or co-locate type descriptors with each function's definition) so the ArrayExpression visitor can look up whether a given array argument should be treated as a typed array rather than a vector, without any function-specific special-casing. I'll update the PR with that generalization.

Comment thread src/webgl/strands_glslBackend.js Outdated
if (t <= p[0]) return c[0]; if (t >= p[1]) return c[1];
return _p5s_paletteLerpSeg(c[0],c[1],p[0],p[1],t);
}
vec4 paletteLerp(vec4[3] c, float[3] p, float t) {
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.

One other thought: is it necessary that these be functions? We could potentially just generate the if statement/loop and return the final result when you call paletteLerp. Then we wouldn't need to generate a GLSL and WGSL version. (If we do stick with this format, we'd need to create a WGSL version too.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's a cleaner approach, generating the interpolation inline would remove the GLSL/WGSL split entirely. For N stops, the transpiler would emit something like:

// paletteLerp with 3 stops, inlined
(t <= p0 ? c0 : t >= p2 ? c2 : t < p1 ? mix(c0, c1, clamp((t-p0)/(p1-p0), 0., 1.)) : mix(c1, c2, clamp((t-p1)/(p2-p1), 0., 1.)))

I'll switch to inline emission in strands_api.js instead of the GLSL helper functions. That also simplifies the backend significantly.

@LalitNarayanYadav LalitNarayanYadav force-pushed the feat/strands-palette-lerp branch 2 times, most recently from d8f9772 to 80c7e89 Compare May 22, 2026 15:47
@LalitNarayanYadav LalitNarayanYadav force-pushed the feat/strands-palette-lerp branch from 80c7e89 to 645ccd1 Compare May 22, 2026 17:00
@LalitNarayanYadav
Copy link
Copy Markdown
Contributor Author

Updated. Both comments addressed:

  1. Replaced _isPaletteLerpArg with a general ARRAY_ARG_FUNCTIONS registry at the top of strands_transpiler.js. The ArrayExpression visitor now walks ancestors to find the nearest CallExpression and checks the registry for whether that argument index should be treated as a raw array. Any future function taking array parameters can register there without touching ArrayExpression again.

  2. Switched to inline IR emission in strands_api.js using buildTernary, mix, clamp, and sub/div — no GLSL helper functions injected, so WGSL works automatically with no separate backend needed. Removed getPaletteLerpGLSL() from strands_glslBackend.js entirely.

All 157 existing shader tests pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants