Skip to content
Merged
Changes from 5 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
148 changes: 148 additions & 0 deletions src/strands/strands_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,154 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
return result;
});

const originalColor = fn.color;
augmentFn(fn, p5, 'color', function (...args) {
if (!strandsContext.active) {
return originalColor.apply(this, args);
}
// Reuse p5's parser - handles hex strings, rgb(), CSS named colors, numerics
const c = originalColor.apply(this, args);
// _getRGBA() returns [r, g, b, a] normalized to 0-1
const rgba = c._getRGBA();

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.

nice!

const { id, dimension } = build.primitiveConstructorNode(

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.

Would return p5.strandsNode(rgba) be equivalent to this? if so it might be a little better at reusing existing functionality.

strandsContext,
{ baseType: BaseType.FLOAT, dimension: null },
rgba
);
return createStrandsNode(id, dimension, strandsContext);
});
const originalLerpColor = fn.lerpColor;
augmentFn(fn, p5, 'lerpColor', function (...args) {
if (!strandsContext.active) {
return originalLerpColor.apply(this, args);
}
// In strands, colors are vec4s - lerpColor maps directly to GLSL mix()
return fn.mix(...args);

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.

Calling these functions on fn makes this no longer be correct, since they're being called as static methods here. I believe we should be calling them like this.mix(...).

I think that means you'll need to pass in this as a parameter into helper functions that you declare outside of these methods, e.g. _rgb2hsb(this, node), and defining that helper like: _rgb2hsb = (instance, node) => { instance.mix(a, b) ... etc.

});
// Component accessors: extract scalar channels from a vec4 color
const originalRed = fn.red;
augmentFn(fn, p5, 'red', function (...args) {
if (!strandsContext.active) {
return originalRed.apply(this, args);
}
return p5.strandsNode(args[0]).x;
});

const originalGreen = fn.green;
augmentFn(fn, p5, 'green', function (...args) {
if (!strandsContext.active) {
return originalGreen.apply(this, args);
}
return p5.strandsNode(args[0]).y;
});

const originalBlue = fn.blue;
augmentFn(fn, p5, 'blue', function (...args) {
if (!strandsContext.active) {
return originalBlue.apply(this, args);
}
return p5.strandsNode(args[0]).z;
});

const originalAlpha = fn.alpha;
augmentFn(fn, p5, 'alpha', function (...args) {
if (!strandsContext.active) {
return originalAlpha.apply(this, args);
}
return p5.strandsNode(args[0]).w;
});

// HSB/HSL accessors — inject conversion helpers and extract channel
const _hsbSnippet = `vec3 _p5_rgb2hsb(vec3 c) {

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.

This is GLSL-specific. Is it possible to either build this out of existing functionality, similar to what you were doing with paletteLerp in #8817? Otherwise we'd need to move these snippets into the glsl and wgsl backends so that we can have a version for either, which is why it's a little less work for each backend if we can make them work without a snippet 🙂

vec4 K = vec4(0.0, -1.0/3.0, 2.0/3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z+(q.w-q.y)/(6.0*d+e)), d/(q.x+e), q.x);
}`;

const _hslSnippet = `vec3 _p5_rgb2hsl(vec3 c) {
float maxC = max(c.r, max(c.g, c.b));
float minC = min(c.r, min(c.g, c.b));
float l = (maxC + minC) / 2.0;
float d = maxC - minC;
float s = d < 1e-10 ? 0.0 : d/(1.0-abs(2.0*l-1.0));
float h = 0.0;
if (d > 1e-10) {
if (maxC == c.r) h = mod((c.g-c.b)/d, 6.0)/6.0;
else if (maxC == c.g) h = ((c.b-c.r)/d+2.0)/6.0;
else h = ((c.r-c.g)/d+4.0)/6.0;
}
return vec3(h, s, l);
}`;

function _injectSnippet(snippet) {
strandsContext.vertexDeclarations.add(snippet);
strandsContext.fragmentDeclarations.add(snippet);
strandsContext.computeDeclarations.add(snippet);
}

const originalHue = fn.hue;
augmentFn(fn, p5, 'hue', function (...args) {
if (!strandsContext.active) {
return originalHue.apply(this, args);
}
_injectSnippet(_hslSnippet);
const colorNode = p5.strandsNode(args[0]);
const { id, dimension } = build.functionCallNode(
strandsContext, '_p5_rgb2hsl',
[fn.vec3(colorNode.x, colorNode.y, colorNode.z)],
{ overloads: [{ params: [DataType.float3], returnType: DataType.float3 }] }
);
return createStrandsNode(id, dimension, strandsContext).x;
});

const originalSaturation = fn.saturation;
augmentFn(fn, p5, 'saturation', function (...args) {
if (!strandsContext.active) {
return originalSaturation.apply(this, args);
}
_injectSnippet(_hslSnippet);
const colorNode = p5.strandsNode(args[0]);
const { id, dimension } = build.functionCallNode(
strandsContext, '_p5_rgb2hsl',
[fn.vec3(colorNode.x, colorNode.y, colorNode.z)],
{ overloads: [{ params: [DataType.float3], returnType: DataType.float3 }] }
);
return createStrandsNode(id, dimension, strandsContext).y;
});

const originalBrightness = fn.brightness;
augmentFn(fn, p5, 'brightness', function (...args) {
if (!strandsContext.active) {
return originalBrightness.apply(this, args);
}
_injectSnippet(_hsbSnippet);
const colorNode = p5.strandsNode(args[0]);
const { id, dimension } = build.functionCallNode(
strandsContext, '_p5_rgb2hsb',
[fn.vec3(colorNode.x, colorNode.y, colorNode.z)],
{ overloads: [{ params: [DataType.float3], returnType: DataType.float3 }] }
);
return createStrandsNode(id, dimension, strandsContext).z;
});

const originalLightness = fn.lightness;
augmentFn(fn, p5, 'lightness', function (...args) {
if (!strandsContext.active) {
return originalLightness.apply(this, args);
}
_injectSnippet(_hslSnippet);
const colorNode = p5.strandsNode(args[0]);
const { id, dimension } = build.functionCallNode(
strandsContext, '_p5_rgb2hsl',
[fn.vec3(colorNode.x, colorNode.y, colorNode.z)],
{ overloads: [{ params: [DataType.float3], returnType: DataType.float3 }] }
);
return createStrandsNode(id, dimension, strandsContext).z;
});

augmentFn(fn, p5, 'getTexture', function (...rawArgs) {
if (strandsContext.active) {
const { id, dimension } = strandsContext.backend.createGetTextureCall(strandsContext, rawArgs);
Expand Down