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
8 changes: 8 additions & 0 deletions src/color/creating_reading.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ function creatingReading(p5, fn){
* The version of `color()` with four parameters interprets them as RGBA, HSBA,
* or HSLA colors, depending on the current `colorMode()`. The last parameter
* sets the alpha (transparency) value.
* In p5.strands shader callbacks, `color()` accepts the same input
* formats but returns a `vec4` instead of a `p5.Color` object, with
* RGBA components normalized to the 0–1 range. All colors in strands
* are RGB-based; `colorMode()` has no effect inside shader callbacks.
* Color utility functions such as `red()`, `green()`, `blue()`,
* `alpha()`, `hue()`, `saturation()`, `brightness()`, and
* `lightness()` also return values in the 0–1 range when used in
* strands.
*
* @method color
* @param {Number} gray number specifying value between white and black.
Expand Down
135 changes: 134 additions & 1 deletion src/strands/strands_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
const originalLerp = fn.lerp;
augmentFn(fn, p5, 'lerp', function (...args) {
if (strandsContext.active) {
return fn.mix(...args);
return this.mix(...args);
} else {
return originalLerp.apply(this, args);
}
Expand All @@ -312,6 +312,139 @@ 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 this.mix(...args);
});
// 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;
});

// RGB to HSB conversion based on:
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB
// Using mix/step to avoid branching, via the compact form from:
// http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl
const _rgb2hsb = (instance, colorNode) => {
const r = colorNode.x;
const g = colorNode.y;
const b = colorNode.z;
const K = instance.vec4(0, -1/3, 2/3, -1);
const p = instance.mix(
instance.vec4(b, g, K.w, K.z),
instance.vec4(g, b, K.x, K.y),
instance.step(b, g)
);
const q = instance.mix(
instance.vec4(p.x, p.y, p.w, r),
instance.vec4(r, p.y, p.z, p.x),
instance.step(p.x, r)
);
const d = q.x.sub(instance.min(q.w, q.y));
const e = p5.strandsNode(1e-10);
const h = instance.abs(q.z.add(q.w.sub(q.y).div(d.mult(6).add(e))));
const s = d.div(q.x.add(e));
return instance.vec3(h, s, q.x);
};

const _rgb2hsl = (instance, colorNode) => {
const r = colorNode.x;
const g = colorNode.y;
const b = colorNode.z;
const maxC = instance.max(r, instance.max(g, b));
const minC = instance.min(r, instance.min(g, b));
const l = maxC.add(minC).div(2);
const d = maxC.sub(minC);
const e = p5.strandsNode(1e-10);
const s = instance.mix(
p5.strandsNode(0),
d.div(p5.strandsNode(1).sub(instance.abs(l.mult(2).sub(1)))),
instance.step(e, d)
);
const h_rg = instance.mod(g.sub(b).div(d.add(e)), p5.strandsNode(6)).div(6);
const h_gb = b.sub(r).div(d.add(e)).add(2).div(6);
const h_br = r.sub(g).div(d.add(e)).add(4).div(6);
const isR = instance.step(maxC.sub(e), r).mult(instance.step(r.sub(e), maxC));
const isG = instance.step(maxC.sub(e), g).mult(instance.step(g.sub(e), maxC));
const h = instance.mix(instance.mix(h_br, h_gb, isG), h_rg, isR);
return instance.vec3(h, s, l);
};
const originalHue = fn.hue;
augmentFn(fn, p5, 'hue', function (...args) {
if (!strandsContext.active) return originalHue.apply(this, args);
const colorNode = p5.strandsNode(args[0]);
return _rgb2hsl(this, this.vec3(colorNode.x, colorNode.y, colorNode.z)).x;
});

const originalSaturation = fn.saturation;
augmentFn(fn, p5, 'saturation', function (...args) {
if (!strandsContext.active) return originalSaturation.apply(this, args);
const colorNode = p5.strandsNode(args[0]);
return _rgb2hsl(this, this.vec3(colorNode.x, colorNode.y, colorNode.z)).y;
});

const originalBrightness = fn.brightness;
augmentFn(fn, p5, 'brightness', function (...args) {
if (!strandsContext.active) return originalBrightness.apply(this, args);
const colorNode = p5.strandsNode(args[0]);
return _rgb2hsb(this, this.vec3(colorNode.x, colorNode.y, colorNode.z)).z;
});

const originalLightness = fn.lightness;
augmentFn(fn, p5, 'lightness', function (...args) {
if (!strandsContext.active) return originalLightness.apply(this, args);
const colorNode = p5.strandsNode(args[0]);
return _rgb2hsl(this, this.vec3(colorNode.x, colorNode.y, colorNode.z)).z;
});

augmentFn(fn, p5, 'getTexture', function (...rawArgs) {
if (strandsContext.active) {
const { id, dimension } = strandsContext.backend.createGetTextureCall(strandsContext, rawArgs);
Expand Down
104 changes: 104 additions & 0 deletions test/unit/webgl/p5.Shader.js
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,110 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca
assert.approximately(middle[0], 128, 10);
assert.approximately(right[0], 204, 10);
});
test('color() with hex string returns correct vec4 in strands', () => {
myp5.createCanvas(50, 50, myp5.WEBGL);
const testShader = myp5.baseMaterialShader().modify(() => {
myp5.getPixelInputs(inputs => {
const c = myp5.color('#ff0000');
inputs.color = [c.x, c.y, c.z, 1.0];
return inputs;
});
}, { myp5 });

myp5.noStroke();
myp5.shader(testShader);
myp5.plane(myp5.width, myp5.height);

const pixelColor = myp5.get(25, 25);
assert.approximately(pixelColor[0], 255, 5);
assert.approximately(pixelColor[1], 0, 5);
assert.approximately(pixelColor[2], 0, 5);
});

test('color() with CSS named color returns correct vec4 in strands', () => {
myp5.createCanvas(50, 50, myp5.WEBGL);
const testShader = myp5.baseMaterialShader().modify(() => {
myp5.getPixelInputs(inputs => {
const c = myp5.color('blue');
inputs.color = [c.x, c.y, c.z, 1.0];
return inputs;
});
}, { myp5 });

myp5.noStroke();
myp5.shader(testShader);
myp5.plane(myp5.width, myp5.height);

const pixelColor = myp5.get(25, 25);
assert.approximately(pixelColor[0], 0, 5);
assert.approximately(pixelColor[1], 0, 5);
assert.approximately(pixelColor[2], 255, 5);
});

test('lerpColor() interpolates between two colors in strands', () => {
myp5.createCanvas(50, 50, myp5.WEBGL);
const testShader = myp5.baseMaterialShader().modify(() => {
myp5.getPixelInputs(inputs => {
const c1 = myp5.color('#ff0000');
const c2 = myp5.color('#0000ff');
const mixed = myp5.lerpColor(c1, c2, 0.5);
inputs.color = [mixed.x, mixed.y, mixed.z, 1.0];
return inputs;
});
}, { myp5 });

myp5.noStroke();
myp5.shader(testShader);
myp5.plane(myp5.width, myp5.height);

const pixelColor = myp5.get(25, 25);
assert.approximately(pixelColor[0], 128, 10);
assert.approximately(pixelColor[1], 0, 5);
assert.approximately(pixelColor[2], 128, 10);
});

test('red(), green(), blue(), alpha() extract correct channels in strands', () => {
myp5.createCanvas(50, 50, myp5.WEBGL);
const testShader = myp5.baseMaterialShader().modify(() => {
myp5.getPixelInputs(inputs => {
const c = myp5.color('#ff8000');
const r = myp5.red(c);
const g = myp5.green(c);
const b = myp5.blue(c);
inputs.color = [r, g, b, 1.0];
return inputs;
});
}, { myp5 });

myp5.noStroke();
myp5.shader(testShader);
myp5.plane(myp5.width, myp5.height);

const pixelColor = myp5.get(25, 25);
assert.approximately(pixelColor[0], 255, 5);
assert.approximately(pixelColor[1], 128, 10);
assert.approximately(pixelColor[2], 0, 5);
});

test('hue() returns normalized hue value in strands', () => {
myp5.createCanvas(50, 50, myp5.WEBGL);
const testShader = myp5.baseMaterialShader().modify(() => {
myp5.getPixelInputs(inputs => {
// Pure red has hue 0
const c = myp5.color('#ff0000');
const h = myp5.hue(c);
inputs.color = [h, h, h, 1.0];
return inputs;
});
}, { myp5 });

myp5.noStroke();
myp5.shader(testShader);
myp5.plane(myp5.width, myp5.height);

const pixelColor = myp5.get(25, 25);
assert.approximately(pixelColor[0], 0, 5);
});

test('handle custom uniform names with automatic values', () => {
myp5.createCanvas(50, 50, myp5.WEBGL);
Expand Down