From ad2ffcddf99857690a3d2d38468ae56422638294 Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Sat, 23 May 2026 00:42:17 +0530 Subject: [PATCH 1/9] feat(strands): add color() support for beginner-friendly color inputs --- src/strands/strands_api.js | 191 ++++++++++++++++++++----------------- 1 file changed, 104 insertions(+), 87 deletions(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 8675dc69b6..754a657e90 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -1,4 +1,4 @@ -import * as build from './ir_builders' +import * as build from './ir_builders'; import { OperatorTable, BlockType, @@ -10,49 +10,49 @@ import { OpCode, StatementType, NodeType, - HOOK_PARAM_PREFIX, + HOOK_PARAM_PREFIX // isNativeType -} from './ir_types' -import { strandsBuiltinFunctions } from './strands_builtins' -import { StrandsConditional } from './strands_conditionals' -import { StrandsFor } from './strands_for' -import { buildTernary } from './strands_ternary' -import * as CFG from './ir_cfg' +} from './ir_types'; +import { strandsBuiltinFunctions } from './strands_builtins'; +import { StrandsConditional } from './strands_conditionals'; +import { StrandsFor } from './strands_for'; +import { buildTernary } from './strands_ternary'; +import * as CFG from './ir_cfg'; import * as DAG from './ir_dag'; -import * as FES from './strands_FES' -import { getNodeDataFromID } from './ir_dag' -import { StrandsNode, createStrandsNode } from './strands_node' +import * as FES from './strands_FES'; +import { getNodeDataFromID } from './ir_dag'; +import { StrandsNode, createStrandsNode } from './strands_node'; const BUILTIN_GLOBAL_SPECS = { - width: { typeInfo: DataType.float1, get: (p) => p.width }, - height: { typeInfo: DataType.float1, get: (p) => p.height }, - mouseX: { typeInfo: DataType.float1, get: (p) => p.mouseX }, - mouseY: { typeInfo: DataType.float1, get: (p) => p.mouseY }, - pmouseX: { typeInfo: DataType.float1, get: (p) => p.pmouseX }, - pmouseY: { typeInfo: DataType.float1, get: (p) => p.pmouseY }, - winMouseX: { typeInfo: DataType.float1, get: (p) => p.winMouseX }, - winMouseY: { typeInfo: DataType.float1, get: (p) => p.winMouseY }, - pwinMouseX: { typeInfo: DataType.float1, get: (p) => p.pwinMouseX }, - pwinMouseY: { typeInfo: DataType.float1, get: (p) => p.pwinMouseY }, - frameCount: { typeInfo: DataType.float1, get: (p) => p.frameCount }, - deltaTime: { typeInfo: DataType.float1, get: (p) => p.deltaTime }, - displayWidth: { typeInfo: DataType.float1, get: (p) => p.displayWidth }, - displayHeight: { typeInfo: DataType.float1, get: (p) => p.displayHeight }, - windowWidth: { typeInfo: DataType.float1, get: (p) => p.windowWidth }, - windowHeight: { typeInfo: DataType.float1, get: (p) => p.windowHeight }, - mouseIsPressed: { typeInfo: DataType.bool1, get: (p) => p.mouseIsPressed }, -} + width: { typeInfo: DataType.float1, get: p => p.width }, + height: { typeInfo: DataType.float1, get: p => p.height }, + mouseX: { typeInfo: DataType.float1, get: p => p.mouseX }, + mouseY: { typeInfo: DataType.float1, get: p => p.mouseY }, + pmouseX: { typeInfo: DataType.float1, get: p => p.pmouseX }, + pmouseY: { typeInfo: DataType.float1, get: p => p.pmouseY }, + winMouseX: { typeInfo: DataType.float1, get: p => p.winMouseX }, + winMouseY: { typeInfo: DataType.float1, get: p => p.winMouseY }, + pwinMouseX: { typeInfo: DataType.float1, get: p => p.pwinMouseX }, + pwinMouseY: { typeInfo: DataType.float1, get: p => p.pwinMouseY }, + frameCount: { typeInfo: DataType.float1, get: p => p.frameCount }, + deltaTime: { typeInfo: DataType.float1, get: p => p.deltaTime }, + displayWidth: { typeInfo: DataType.float1, get: p => p.displayWidth }, + displayHeight: { typeInfo: DataType.float1, get: p => p.displayHeight }, + windowWidth: { typeInfo: DataType.float1, get: p => p.windowWidth }, + windowHeight: { typeInfo: DataType.float1, get: p => p.windowHeight }, + mouseIsPressed: { typeInfo: DataType.bool1, get: p => p.mouseIsPressed } +}; function _getBuiltinGlobalsCache(strandsContext) { if (!strandsContext._builtinGlobals || strandsContext._builtinGlobals.dag !== strandsContext.dag) { strandsContext._builtinGlobals = { dag: strandsContext.dag, nodes: new Map(), - uniformsAdded: new Set(), - } + uniformsAdded: new Set() + }; } // return the cache - return strandsContext._builtinGlobals + return strandsContext._builtinGlobals; } function getOrCreateUniformNode(strandsContext, uniformName, typeInfo, defaultValueFn) { @@ -66,7 +66,7 @@ function getOrCreateUniformNode(strandsContext, uniformName, typeInfo, defaultVa strandsContext.uniforms.push({ name: uniformName, typeInfo, - defaultValue: defaultValueFn, + defaultValue: defaultValueFn }); } @@ -97,23 +97,23 @@ function getBuiltinGlobalNode(strandsContext, name) { } function installBuiltinGlobalAccessors(strandsContext) { - if (strandsContext._builtinGlobalsAccessorsInstalled) return + if (strandsContext._builtinGlobalsAccessorsInstalled) return; - const getRuntimeP5Instance = () => strandsContext.renderer?._pInst || strandsContext.p5?.instance + const getRuntimeP5Instance = () => strandsContext.renderer?._pInst || strandsContext.p5?.instance; for (const name of Object.keys(BUILTIN_GLOBAL_SPECS)) { - const spec = BUILTIN_GLOBAL_SPECS[name] + const spec = BUILTIN_GLOBAL_SPECS[name]; Object.defineProperty(window, name, { get: () => { if (strandsContext.active) { return getBuiltinGlobalNode(strandsContext, name); } - const inst = getRuntimeP5Instance() - return spec.get(inst); - }, - }) + const inst = getRuntimeP5Instance(); + return spec.get(inst); + } + }); } - strandsContext._builtinGlobalsAccessorsInstalled = true + strandsContext._builtinGlobalsAccessorsInstalled = true; } ////////////////////////////////////////////// @@ -169,7 +169,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { p5[name] = function (nodeOrValue) { const { id, dimension } = build.unaryOpNode(strandsContext, nodeOrValue, opCode); return createStrandsNode(id, dimension, strandsContext); - } + }; } } ////////////////////////////////////////////// @@ -190,7 +190,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { // Some methods need to be used by both. p5.strandsIf = function(conditionNode, ifBody) { return new StrandsConditional(strandsContext, conditionNode, ifBody); - } + }; augmentFn(fn, p5, 'strandsIf', p5.strandsIf); p5.strandsFor = function(initialCb, conditionCb, updateCb, bodyCb, initialVars) { return new StrandsFor(strandsContext, initialCb, conditionCb, updateCb, bodyCb, initialVars).build(); @@ -238,7 +238,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return args[0]; } if (args.length > 4) { - FES.userError("type error", "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") + FES.userError('type error', "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported."); } // Filter out undefined/null values const flatArgs = args.flat(); @@ -255,7 +255,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const { id, dimension } = build.primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, definedArgs); return createStrandsNode(id, dimension, strandsContext);//new StrandsNode(id, dimension, strandsContext); - } + }; ////////////////////////////////////////////// // Builtins, uniforms, variable constructors ////////////////////////////////////////////// @@ -279,7 +279,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } else { p5._friendlyError( `It looks like you've called ${functionName} outside of a shader's modify() function.` - ) + ); } }); } @@ -312,14 +312,31 @@ 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(); + const { id, dimension } = build.primitiveConstructorNode( + strandsContext, + { baseType: BaseType.FLOAT, dimension: null }, + rgba + ); + return createStrandsNode(id, dimension, strandsContext); + }); + augmentFn(fn, p5, 'getTexture', function (...rawArgs) { if (strandsContext.active) { const { id, dimension } = strandsContext.backend.createGetTextureCall(strandsContext, rawArgs); return createStrandsNode(id, dimension, strandsContext); } else { p5._friendlyError( - `It looks like you've called getTexture outside of a shader's modify() function.` - ) + 'It looks like you\'ve called getTexture outside of a shader\'s modify() function.' + ); } }); @@ -396,7 +413,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const { id, dimension } = build.functionCallNode(strandsContext, 'noise', nodeArgs, { overloads: [{ params: [DataType.float3, DataType.int1, DataType.float1], - returnType: DataType.float1, + returnType: DataType.float1 }] }); return createStrandsNode(id, dimension, strandsContext); @@ -438,7 +455,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { DataType.float1, userSeed !== null ? () => userSeed - : () => performance.now(), + : () => performance.now() ); } @@ -448,25 +465,25 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const nodeArgs = [seedNode]; const randomOverloads = [{ params: [DataType.float1], - returnType: DataType.float1, + returnType: DataType.float1 }]; if (args.length === 0) { const { id, dimension } = build.functionCallNode(strandsContext, 'random', nodeArgs, { - overloads: randomOverloads, + overloads: randomOverloads }); return createStrandsNode(id, dimension, strandsContext); } else if (args.length === 1) { // random(max) → [0, max) const rawNode = build.functionCallNode(strandsContext, 'random', nodeArgs, { - overloads: randomOverloads, + overloads: randomOverloads }); const rawStrandsNode = createStrandsNode(rawNode.id, rawNode.dimension, strandsContext); return rawStrandsNode.mult(p5.strandsNode(args[0])); } else if (args.length === 2) { // random(min, max) → [min, max) const rawNode = build.functionCallNode(strandsContext, 'random', nodeArgs, { - overloads: randomOverloads, + overloads: randomOverloads }); const rawStrandsNode = createStrandsNode(rawNode.id, rawNode.dimension, strandsContext); const minNode = p5.strandsNode(args[0]); @@ -517,7 +534,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { pascalTypeName = typeInfo.fnName.charAt(0).toUpperCase() + typeInfo.fnName.slice(1); if (pascalTypeName === 'Sampler2D') { - typeAliases.push('Texture') + typeAliases.push('Texture'); } else if (/^vec/.test(typeInfo.fnName)) { typeAliases.push(pascalTypeName.replace('Vec', 'Vector')); } @@ -540,7 +557,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { strandsContext.sharedVariables.set(name, { typeInfo, usedInVertex: false, - usedInFragment: false, + usedInFragment: false }); return createStrandsNode(id, dimension, strandsContext); @@ -567,7 +584,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { { overloads: [{ params: [args[0].typeInfo()], - returnType: typeInfo, + returnType: typeInfo }] } ); @@ -626,7 +643,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { strandsContext.uniforms.push({ name, typeInfo: { baseType: 'storage', dimension: 1, schema }, - defaultValue, + defaultValue }); // Create StrandsNode with _originalIdentifier set (like varying variables) @@ -656,8 +673,8 @@ function createHookArguments(strandsContext, parameters){ const propertyType = structTypeInfo.properties[i]; Object.defineProperty(structNode, propertyType.name, { get() { - const propNode = getNodeDataFromID(dag, dag.dependsOn[structNode.id][i]) - const onRebind = (newFieldID) => { + const propNode = getNodeDataFromID(dag, dag.dependsOn[structNode.id][i]); + const onRebind = newFieldID => { const oldDeps = dag.dependsOn[structNode.id]; const newDeps = oldDeps.slice(); newDeps[i] = newFieldID; @@ -685,7 +702,7 @@ function createHookArguments(strandsContext, parameters){ const newStructInfo = build.structInstanceNode(strandsContext, structTypeInfo, `${HOOK_PARAM_PREFIX}${param.name}`, newDependsOn); structNode.id = newStructInfo.id; } - }) + }); } args.push(structNode); } @@ -708,31 +725,31 @@ function createHookArguments(strandsContext, parameters){ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName) { if (!(returned?.isStrandsNode)) { // try { - const result = build.primitiveConstructorNode(strandsContext, expectedType, returned); - return result.id; + const result = build.primitiveConstructorNode(strandsContext, expectedType, returned); + return result.id; // } catch (e) { - // FES.userError('type error', - // `There was a type mismatch for a value returned from ${hookName}.\n` + - // `The value in question was supposed to be:\n` + - // `${expectedType.baseType + expectedType.dimension}\n` + - // `But you returned:\n` + - // `${returned}` - // ); + // FES.userError('type error', + // `There was a type mismatch for a value returned from ${hookName}.\n` + + // `The value in question was supposed to be:\n` + + // `${expectedType.baseType + expectedType.dimension}\n` + + // `But you returned:\n` + + // `${returned}` + // ); // } } const dag = strandsContext.dag; let returnedNodeID = returned.id; const receivedType = { baseType: dag.baseTypes[returnedNodeID], - dimension: dag.dimensions[returnedNodeID], - } + dimension: dag.dimensions[returnedNodeID] + }; if (receivedType.dimension !== expectedType.dimension) { if (receivedType.dimension !== 1) { const receivedTypeDisplay = receivedType.baseType + (receivedType.dimension > 1 ? receivedType.dimension : ''); const expectedTypeDisplay = expectedType.baseType + expectedType.dimension; FES.userError('type error', `You have returned a ${receivedTypeDisplay} in ${hookName} when a ${expectedTypeDisplay} was expected!\n\n` + - `Make sure your hook returns the correct type.` + 'Make sure your hook returns the correct type.' ); } else { @@ -747,7 +764,7 @@ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName return returnedNodeID; } export function createShaderHooksFunctions(strandsContext, fn, shader) { - installBuiltinGlobalAccessors(strandsContext) + installBuiltinGlobalAccessors(strandsContext); // Add shader context to hooks before spreading const vertexHooksWithContext = Object.fromEntries( @@ -763,8 +780,8 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const availableHooks = { ...vertexHooksWithContext, ...fragmentHooksWithContext, - ...computeHooksWithContext, - } + ...computeHooksWithContext + }; const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); const { cfg, dag } = strandsContext; @@ -773,7 +790,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const args = setupHook(); hook._result = hookUserCallback(...args) ?? hook._result; finishHook(); - } + }; // In the flat strands API, this is how result-returning hooks // are used @@ -805,7 +822,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { set(val) { args[argIdx][key] = val; }, - enumerable: true, + enumerable: true }); } if (hookType.returnType?.typeName === hookType.parameters[argIdx].type.typeName) { @@ -825,7 +842,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const expectedReturnType = hookType.returnType; let rootNodeID = null; - const handleRetVal = (retNode) => { + const handleRetVal = retNode => { if(isStructType(expectedReturnType)) { const expectedStructType = structType(expectedReturnType); if (retNode?.isStrandsNode) { @@ -842,12 +859,12 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { `You have returned a ${receivedTypeDisplay} from ${hookType.name} when a ${expectedStructType.typeName} was expected.\n\n` + `The ${expectedStructType.typeName} struct has these properties: { ${expectedProps} }\n\n` + `Instead of returning a different type, you should modify and return the ${expectedStructType.typeName} struct that was passed to your hook.\n\n` + - `For example:\n` + + 'For example:\n' + `${hookType.name}((inputs) => {\n` + - ` // Modify properties of inputs\n` + - ` inputs.someProperty = ...;\n` + - ` return inputs; // Return the modified struct\n` + - `})` + ' // Modify properties of inputs\n' + + ' inputs.someProperty = ...;\n' + + ' return inputs; // Return the modified struct\n' + + '})' ); } const newDeps = returnedNode.dependsOn.slice(); @@ -873,7 +890,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { `You've returned an incomplete ${expectedStructType.typeName} struct from ${hookType.name}.\n\n` + `Expected properties: { ${expectedProps} }\n` + `Received properties: { ${receivedProps} }\n\n` + - `All properties are required! Make sure to include all properties in the returned struct.` + 'All properties are required! Make sure to include all properties in the returned struct.' ); } const expectedTypeInfo = expectedProp.dataType; @@ -891,7 +908,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const expectedTypeInfo = expectedReturnType.dataType; return enforceReturnTypeMatch(strandsContext, expectedTypeInfo, retNode, hookType.name); } - } + }; for (const { valueNode, earlyReturnID } of hook.earlyReturns) { const id = handleRetVal(valueNode); if (id !== null) { @@ -907,7 +924,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { hookType, entryBlockID, rootNodeID, - shaderContext: hookInfo?.shaderContext, // 'vertex', 'fragment', or 'compute' + shaderContext: hookInfo?.shaderContext // 'vertex', 'fragment', or 'compute' }); CFG.popBlock(cfg); }; From 3df6b4a64de2b571b1b2818cb12c275db121909a Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Sat, 23 May 2026 17:01:20 +0530 Subject: [PATCH 2/9] feat(strands): add lerpColor() support mapping to GLSL mix() --- src/strands/strands_api.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 754a657e90..e977852cff 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -329,6 +329,15 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { 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); + }); + augmentFn(fn, p5, 'getTexture', function (...rawArgs) { if (strandsContext.active) { const { id, dimension } = strandsContext.backend.createGetTextureCall(strandsContext, rawArgs); From 4c6308818a7ddc30014918c912c8ffefb76735ef Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Sat, 23 May 2026 17:07:08 +0530 Subject: [PATCH 3/9] feat(strands): add red(), green(), blue(), alpha() component accessors --- src/strands/strands_api.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index e977852cff..6da7dae917 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -338,6 +338,39 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return fn.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; + }); + augmentFn(fn, p5, 'getTexture', function (...rawArgs) { if (strandsContext.active) { const { id, dimension } = strandsContext.backend.createGetTextureCall(strandsContext, rawArgs); From ddc668a06e43ef7012472ad664290fb4d3df9499 Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Sat, 23 May 2026 21:24:45 +0530 Subject: [PATCH 4/9] feat(strands): add hue(), saturation(), brightness(), lightness() accessors --- src/strands/strands_api.js | 91 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 6da7dae917..d14f232417 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -371,6 +371,97 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return p5.strandsNode(args[0]).w; }); + // HSB/HSL accessors — inject conversion helpers and extract channel + const _hsbSnippet = `vec3 _p5_rgb2hsb(vec3 c) { + 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); From 6684e175c478e67960ae3078e3f0d78b21093c0d Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Thu, 28 May 2026 03:54:53 +0530 Subject: [PATCH 5/9] feat(strands): add beginner-friendly color support in p5.strands --- src/strands/strands_api.js | 186 ++++++++++++++++++------------------- 1 file changed, 92 insertions(+), 94 deletions(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index d14f232417..996179e6a9 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -1,4 +1,4 @@ -import * as build from './ir_builders'; +import * as build from './ir_builders' import { OperatorTable, BlockType, @@ -10,49 +10,49 @@ import { OpCode, StatementType, NodeType, - HOOK_PARAM_PREFIX + HOOK_PARAM_PREFIX, // isNativeType -} from './ir_types'; -import { strandsBuiltinFunctions } from './strands_builtins'; -import { StrandsConditional } from './strands_conditionals'; -import { StrandsFor } from './strands_for'; -import { buildTernary } from './strands_ternary'; -import * as CFG from './ir_cfg'; +} from './ir_types' +import { strandsBuiltinFunctions } from './strands_builtins' +import { StrandsConditional } from './strands_conditionals' +import { StrandsFor } from './strands_for' +import { buildTernary } from './strands_ternary' +import * as CFG from './ir_cfg' import * as DAG from './ir_dag'; -import * as FES from './strands_FES'; -import { getNodeDataFromID } from './ir_dag'; -import { StrandsNode, createStrandsNode } from './strands_node'; +import * as FES from './strands_FES' +import { getNodeDataFromID } from './ir_dag' +import { StrandsNode, createStrandsNode } from './strands_node' const BUILTIN_GLOBAL_SPECS = { - width: { typeInfo: DataType.float1, get: p => p.width }, - height: { typeInfo: DataType.float1, get: p => p.height }, - mouseX: { typeInfo: DataType.float1, get: p => p.mouseX }, - mouseY: { typeInfo: DataType.float1, get: p => p.mouseY }, - pmouseX: { typeInfo: DataType.float1, get: p => p.pmouseX }, - pmouseY: { typeInfo: DataType.float1, get: p => p.pmouseY }, - winMouseX: { typeInfo: DataType.float1, get: p => p.winMouseX }, - winMouseY: { typeInfo: DataType.float1, get: p => p.winMouseY }, - pwinMouseX: { typeInfo: DataType.float1, get: p => p.pwinMouseX }, - pwinMouseY: { typeInfo: DataType.float1, get: p => p.pwinMouseY }, - frameCount: { typeInfo: DataType.float1, get: p => p.frameCount }, - deltaTime: { typeInfo: DataType.float1, get: p => p.deltaTime }, - displayWidth: { typeInfo: DataType.float1, get: p => p.displayWidth }, - displayHeight: { typeInfo: DataType.float1, get: p => p.displayHeight }, - windowWidth: { typeInfo: DataType.float1, get: p => p.windowWidth }, - windowHeight: { typeInfo: DataType.float1, get: p => p.windowHeight }, - mouseIsPressed: { typeInfo: DataType.bool1, get: p => p.mouseIsPressed } -}; + width: { typeInfo: DataType.float1, get: (p) => p.width }, + height: { typeInfo: DataType.float1, get: (p) => p.height }, + mouseX: { typeInfo: DataType.float1, get: (p) => p.mouseX }, + mouseY: { typeInfo: DataType.float1, get: (p) => p.mouseY }, + pmouseX: { typeInfo: DataType.float1, get: (p) => p.pmouseX }, + pmouseY: { typeInfo: DataType.float1, get: (p) => p.pmouseY }, + winMouseX: { typeInfo: DataType.float1, get: (p) => p.winMouseX }, + winMouseY: { typeInfo: DataType.float1, get: (p) => p.winMouseY }, + pwinMouseX: { typeInfo: DataType.float1, get: (p) => p.pwinMouseX }, + pwinMouseY: { typeInfo: DataType.float1, get: (p) => p.pwinMouseY }, + frameCount: { typeInfo: DataType.float1, get: (p) => p.frameCount }, + deltaTime: { typeInfo: DataType.float1, get: (p) => p.deltaTime }, + displayWidth: { typeInfo: DataType.float1, get: (p) => p.displayWidth }, + displayHeight: { typeInfo: DataType.float1, get: (p) => p.displayHeight }, + windowWidth: { typeInfo: DataType.float1, get: (p) => p.windowWidth }, + windowHeight: { typeInfo: DataType.float1, get: (p) => p.windowHeight }, + mouseIsPressed: { typeInfo: DataType.bool1, get: (p) => p.mouseIsPressed }, +} function _getBuiltinGlobalsCache(strandsContext) { if (!strandsContext._builtinGlobals || strandsContext._builtinGlobals.dag !== strandsContext.dag) { strandsContext._builtinGlobals = { dag: strandsContext.dag, nodes: new Map(), - uniformsAdded: new Set() - }; + uniformsAdded: new Set(), + } } // return the cache - return strandsContext._builtinGlobals; + return strandsContext._builtinGlobals } function getOrCreateUniformNode(strandsContext, uniformName, typeInfo, defaultValueFn) { @@ -66,7 +66,7 @@ function getOrCreateUniformNode(strandsContext, uniformName, typeInfo, defaultVa strandsContext.uniforms.push({ name: uniformName, typeInfo, - defaultValue: defaultValueFn + defaultValue: defaultValueFn, }); } @@ -97,23 +97,23 @@ function getBuiltinGlobalNode(strandsContext, name) { } function installBuiltinGlobalAccessors(strandsContext) { - if (strandsContext._builtinGlobalsAccessorsInstalled) return; + if (strandsContext._builtinGlobalsAccessorsInstalled) return - const getRuntimeP5Instance = () => strandsContext.renderer?._pInst || strandsContext.p5?.instance; + const getRuntimeP5Instance = () => strandsContext.renderer?._pInst || strandsContext.p5?.instance for (const name of Object.keys(BUILTIN_GLOBAL_SPECS)) { - const spec = BUILTIN_GLOBAL_SPECS[name]; + const spec = BUILTIN_GLOBAL_SPECS[name] Object.defineProperty(window, name, { get: () => { if (strandsContext.active) { return getBuiltinGlobalNode(strandsContext, name); } - const inst = getRuntimeP5Instance(); - return spec.get(inst); - } - }); + const inst = getRuntimeP5Instance() + return spec.get(inst); + }, + }) } - strandsContext._builtinGlobalsAccessorsInstalled = true; + strandsContext._builtinGlobalsAccessorsInstalled = true } ////////////////////////////////////////////// @@ -169,7 +169,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { p5[name] = function (nodeOrValue) { const { id, dimension } = build.unaryOpNode(strandsContext, nodeOrValue, opCode); return createStrandsNode(id, dimension, strandsContext); - }; + } } } ////////////////////////////////////////////// @@ -190,7 +190,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { // Some methods need to be used by both. p5.strandsIf = function(conditionNode, ifBody) { return new StrandsConditional(strandsContext, conditionNode, ifBody); - }; + } augmentFn(fn, p5, 'strandsIf', p5.strandsIf); p5.strandsFor = function(initialCb, conditionCb, updateCb, bodyCb, initialVars) { return new StrandsFor(strandsContext, initialCb, conditionCb, updateCb, bodyCb, initialVars).build(); @@ -238,7 +238,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return args[0]; } if (args.length > 4) { - FES.userError('type error', "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported."); + FES.userError("type error", "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.") } // Filter out undefined/null values const flatArgs = args.flat(); @@ -255,7 +255,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const { id, dimension } = build.primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, definedArgs); return createStrandsNode(id, dimension, strandsContext);//new StrandsNode(id, dimension, strandsContext); - }; + } ////////////////////////////////////////////// // Builtins, uniforms, variable constructors ////////////////////////////////////////////// @@ -279,7 +279,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } else { p5._friendlyError( `It looks like you've called ${functionName} outside of a shader's modify() function.` - ); + ) } }); } @@ -317,9 +317,9 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { if (!strandsContext.active) { return originalColor.apply(this, args); } - // Reuse p5's parser — handles hex strings, rgb(), CSS named colors, numerics + // 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 + // _getRGBA() returns [r, g, b, a] normalized to 0-1 const rgba = c._getRGBA(); const { id, dimension } = build.primitiveConstructorNode( strandsContext, @@ -328,16 +328,14 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { ); 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() + // In strands, colors are vec4s - lerpColor maps directly to GLSL mix() return fn.mix(...args); }); - // Component accessors: extract scalar channels from a vec4 color const originalRed = fn.red; augmentFn(fn, p5, 'red', function (...args) { @@ -379,7 +377,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { 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)); @@ -394,7 +392,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { else h = ((c.r-c.g)/d+4.0)/6.0; } return vec3(h, s, l); - }`; +}`; function _injectSnippet(snippet) { strandsContext.vertexDeclarations.add(snippet); @@ -468,8 +466,8 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return createStrandsNode(id, dimension, strandsContext); } else { p5._friendlyError( - 'It looks like you\'ve called getTexture outside of a shader\'s modify() function.' - ); + `It looks like you've called getTexture outside of a shader's modify() function.` + ) } }); @@ -546,7 +544,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const { id, dimension } = build.functionCallNode(strandsContext, 'noise', nodeArgs, { overloads: [{ params: [DataType.float3, DataType.int1, DataType.float1], - returnType: DataType.float1 + returnType: DataType.float1, }] }); return createStrandsNode(id, dimension, strandsContext); @@ -588,7 +586,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { DataType.float1, userSeed !== null ? () => userSeed - : () => performance.now() + : () => performance.now(), ); } @@ -598,25 +596,25 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const nodeArgs = [seedNode]; const randomOverloads = [{ params: [DataType.float1], - returnType: DataType.float1 + returnType: DataType.float1, }]; if (args.length === 0) { const { id, dimension } = build.functionCallNode(strandsContext, 'random', nodeArgs, { - overloads: randomOverloads + overloads: randomOverloads, }); return createStrandsNode(id, dimension, strandsContext); } else if (args.length === 1) { // random(max) → [0, max) const rawNode = build.functionCallNode(strandsContext, 'random', nodeArgs, { - overloads: randomOverloads + overloads: randomOverloads, }); const rawStrandsNode = createStrandsNode(rawNode.id, rawNode.dimension, strandsContext); return rawStrandsNode.mult(p5.strandsNode(args[0])); } else if (args.length === 2) { // random(min, max) → [min, max) const rawNode = build.functionCallNode(strandsContext, 'random', nodeArgs, { - overloads: randomOverloads + overloads: randomOverloads, }); const rawStrandsNode = createStrandsNode(rawNode.id, rawNode.dimension, strandsContext); const minNode = p5.strandsNode(args[0]); @@ -667,7 +665,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { pascalTypeName = typeInfo.fnName.charAt(0).toUpperCase() + typeInfo.fnName.slice(1); if (pascalTypeName === 'Sampler2D') { - typeAliases.push('Texture'); + typeAliases.push('Texture') } else if (/^vec/.test(typeInfo.fnName)) { typeAliases.push(pascalTypeName.replace('Vec', 'Vector')); } @@ -690,7 +688,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { strandsContext.sharedVariables.set(name, { typeInfo, usedInVertex: false, - usedInFragment: false + usedInFragment: false, }); return createStrandsNode(id, dimension, strandsContext); @@ -717,7 +715,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { { overloads: [{ params: [args[0].typeInfo()], - returnType: typeInfo + returnType: typeInfo, }] } ); @@ -776,7 +774,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { strandsContext.uniforms.push({ name, typeInfo: { baseType: 'storage', dimension: 1, schema }, - defaultValue + defaultValue, }); // Create StrandsNode with _originalIdentifier set (like varying variables) @@ -806,8 +804,8 @@ function createHookArguments(strandsContext, parameters){ const propertyType = structTypeInfo.properties[i]; Object.defineProperty(structNode, propertyType.name, { get() { - const propNode = getNodeDataFromID(dag, dag.dependsOn[structNode.id][i]); - const onRebind = newFieldID => { + const propNode = getNodeDataFromID(dag, dag.dependsOn[structNode.id][i]) + const onRebind = (newFieldID) => { const oldDeps = dag.dependsOn[structNode.id]; const newDeps = oldDeps.slice(); newDeps[i] = newFieldID; @@ -835,7 +833,7 @@ function createHookArguments(strandsContext, parameters){ const newStructInfo = build.structInstanceNode(strandsContext, structTypeInfo, `${HOOK_PARAM_PREFIX}${param.name}`, newDependsOn); structNode.id = newStructInfo.id; } - }); + }) } args.push(structNode); } @@ -858,31 +856,31 @@ function createHookArguments(strandsContext, parameters){ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName) { if (!(returned?.isStrandsNode)) { // try { - const result = build.primitiveConstructorNode(strandsContext, expectedType, returned); - return result.id; + const result = build.primitiveConstructorNode(strandsContext, expectedType, returned); + return result.id; // } catch (e) { - // FES.userError('type error', - // `There was a type mismatch for a value returned from ${hookName}.\n` + - // `The value in question was supposed to be:\n` + - // `${expectedType.baseType + expectedType.dimension}\n` + - // `But you returned:\n` + - // `${returned}` - // ); + // FES.userError('type error', + // `There was a type mismatch for a value returned from ${hookName}.\n` + + // `The value in question was supposed to be:\n` + + // `${expectedType.baseType + expectedType.dimension}\n` + + // `But you returned:\n` + + // `${returned}` + // ); // } } const dag = strandsContext.dag; let returnedNodeID = returned.id; const receivedType = { baseType: dag.baseTypes[returnedNodeID], - dimension: dag.dimensions[returnedNodeID] - }; + dimension: dag.dimensions[returnedNodeID], + } if (receivedType.dimension !== expectedType.dimension) { if (receivedType.dimension !== 1) { const receivedTypeDisplay = receivedType.baseType + (receivedType.dimension > 1 ? receivedType.dimension : ''); const expectedTypeDisplay = expectedType.baseType + expectedType.dimension; FES.userError('type error', `You have returned a ${receivedTypeDisplay} in ${hookName} when a ${expectedTypeDisplay} was expected!\n\n` + - 'Make sure your hook returns the correct type.' + `Make sure your hook returns the correct type.` ); } else { @@ -897,7 +895,7 @@ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName return returnedNodeID; } export function createShaderHooksFunctions(strandsContext, fn, shader) { - installBuiltinGlobalAccessors(strandsContext); + installBuiltinGlobalAccessors(strandsContext) // Add shader context to hooks before spreading const vertexHooksWithContext = Object.fromEntries( @@ -913,8 +911,8 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const availableHooks = { ...vertexHooksWithContext, ...fragmentHooksWithContext, - ...computeHooksWithContext - }; + ...computeHooksWithContext, + } const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name)); const { cfg, dag } = strandsContext; @@ -923,7 +921,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const args = setupHook(); hook._result = hookUserCallback(...args) ?? hook._result; finishHook(); - }; + } // In the flat strands API, this is how result-returning hooks // are used @@ -955,7 +953,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { set(val) { args[argIdx][key] = val; }, - enumerable: true + enumerable: true, }); } if (hookType.returnType?.typeName === hookType.parameters[argIdx].type.typeName) { @@ -975,7 +973,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const expectedReturnType = hookType.returnType; let rootNodeID = null; - const handleRetVal = retNode => { + const handleRetVal = (retNode) => { if(isStructType(expectedReturnType)) { const expectedStructType = structType(expectedReturnType); if (retNode?.isStrandsNode) { @@ -992,12 +990,12 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { `You have returned a ${receivedTypeDisplay} from ${hookType.name} when a ${expectedStructType.typeName} was expected.\n\n` + `The ${expectedStructType.typeName} struct has these properties: { ${expectedProps} }\n\n` + `Instead of returning a different type, you should modify and return the ${expectedStructType.typeName} struct that was passed to your hook.\n\n` + - 'For example:\n' + + `For example:\n` + `${hookType.name}((inputs) => {\n` + - ' // Modify properties of inputs\n' + - ' inputs.someProperty = ...;\n' + - ' return inputs; // Return the modified struct\n' + - '})' + ` // Modify properties of inputs\n` + + ` inputs.someProperty = ...;\n` + + ` return inputs; // Return the modified struct\n` + + `})` ); } const newDeps = returnedNode.dependsOn.slice(); @@ -1023,7 +1021,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { `You've returned an incomplete ${expectedStructType.typeName} struct from ${hookType.name}.\n\n` + `Expected properties: { ${expectedProps} }\n` + `Received properties: { ${receivedProps} }\n\n` + - 'All properties are required! Make sure to include all properties in the returned struct.' + `All properties are required! Make sure to include all properties in the returned struct.` ); } const expectedTypeInfo = expectedProp.dataType; @@ -1041,7 +1039,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const expectedTypeInfo = expectedReturnType.dataType; return enforceReturnTypeMatch(strandsContext, expectedTypeInfo, retNode, hookType.name); } - }; + } for (const { valueNode, earlyReturnID } of hook.earlyReturns) { const id = handleRetVal(valueNode); if (id !== null) { @@ -1057,7 +1055,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { hookType, entryBlockID, rootNodeID, - shaderContext: hookInfo?.shaderContext // 'vertex', 'fragment', or 'compute' + shaderContext: hookInfo?.shaderContext, // 'vertex', 'fragment', or 'compute' }); CFG.popBlock(cfg); }; From 6b641dc4ecba6fc1362a69b2ab07cffc011bca1e Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Tue, 2 Jun 2026 16:46:38 +0530 Subject: [PATCH 6/9] feat(strands): rewrite HSB/HSL accessors using backend-agnostic strands ops --- src/strands/strands_api.js | 123 +++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 67 deletions(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 996179e6a9..116d72b14a 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -369,95 +369,84 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return p5.strandsNode(args[0]).w; }); - // HSB/HSL accessors — inject conversion helpers and extract channel - const _hsbSnippet = `vec3 _p5_rgb2hsb(vec3 c) { - 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; + // Helper: RGB vec3 → HSB vec3 using backend-agnostic strands ops + const _rgb2hsb = (colorNode) => { + const r = colorNode.x; + const g = colorNode.y; + const b = colorNode.z; + // vec4 K = vec4(0, -1/3, 2/3, -1) + const K = fn.vec4(0.0, -1.0/3.0, 2.0/3.0, -1.0); + // p = mix(vec4(b, g, K.w, K.z), vec4(g, b, K.x, K.y), step(b, g)) + const p = fn.mix( + fn.vec4(b, g, K.w, K.z), + fn.vec4(g, b, K.x, K.y), + fn.step(b, g) + ); + // q = mix(vec4(p.x, p.y, p.w, r), vec4(r, p.y, p.z, p.x), step(p.x, r)) + const q = fn.mix( + fn.vec4(p.x, p.y, p.w, r), + fn.vec4(r, p.y, p.z, p.x), + fn.step(p.x, r) + ); + const d = q.x.sub(fn.min(q.w, q.y)); + const e = p5.strandsNode(1.0e-10); + const h = fn.abs(q.z.add(q.w.sub(q.y).div(d.mult(6.0).add(e)))); + const s = d.div(q.x.add(e)); + const v = q.x; + return fn.vec3(h, s, v); } - return vec3(h, s, l); -}`; - function _injectSnippet(snippet) { - strandsContext.vertexDeclarations.add(snippet); - strandsContext.fragmentDeclarations.add(snippet); - strandsContext.computeDeclarations.add(snippet); + // Helper: RGB vec3 → HSL vec3 using backend-agnostic strands ops + const _rgb2hsl = (colorNode) => { + const r = colorNode.x; + const g = colorNode.y; + const b = colorNode.z; + const maxC = fn.max(r, fn.max(g, b)); + const minC = fn.min(r, fn.min(g, b)); + const l = maxC.add(minC).div(2.0); + const d = maxC.sub(minC); + const e = p5.strandsNode(1.0e-10); + const s = fn.mix( + p5.strandsNode(0.0), + d.div(p5.strandsNode(1.0).sub(fn.abs(l.mult(2.0).sub(1.0)))), + fn.step(e, d) + ); + // Hue: use mix+step to avoid conditionals + const h_rg = fn.mod(g.sub(b).div(d.add(e)), p5.strandsNode(6.0)).div(6.0); + const h_gb = b.sub(r).div(d.add(e)).add(2.0).div(6.0); + const h_br = r.sub(g).div(d.add(e)).add(4.0).div(6.0); + const isR = fn.step(maxC.sub(e), r).mult(fn.step(r.sub(e), maxC)); + const isG = fn.step(maxC.sub(e), g).mult(fn.step(g.sub(e), maxC)); + const h = fn.mix(fn.mix(h_br, h_gb, isG), h_rg, isR); + return fn.vec3(h, s, l); } const originalHue = fn.hue; augmentFn(fn, p5, 'hue', function (...args) { - if (!strandsContext.active) { - return originalHue.apply(this, args); - } - _injectSnippet(_hslSnippet); + if (!strandsContext.active) return originalHue.apply(this, args); 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; + return _rgb2hsl(fn.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); - } - _injectSnippet(_hslSnippet); + if (!strandsContext.active) return originalSaturation.apply(this, args); 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; + return _rgb2hsl(fn.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); - } - _injectSnippet(_hsbSnippet); + if (!strandsContext.active) return originalBrightness.apply(this, args); 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; + return _rgb2hsb(fn.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); - } - _injectSnippet(_hslSnippet); + if (!strandsContext.active) return originalLightness.apply(this, args); 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; + return _rgb2hsl(fn.vec3(colorNode.x, colorNode.y, colorNode.z)).z; }); augmentFn(fn, p5, 'getTexture', function (...rawArgs) { From ebc7fd598218147579d2c0b8ad05749a63be2042 Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Thu, 4 Jun 2026 00:57:44 +0530 Subject: [PATCH 7/9] feat(strands): fix HSB/HSL helpers to use instance methods and add formula references --- src/strands/strands_api.js | 112 ++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 58 deletions(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 116d72b14a..1756b5b628 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -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); } @@ -334,7 +334,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return originalLerpColor.apply(this, args); } // In strands, colors are vec4s - lerpColor maps directly to GLSL mix() - return fn.mix(...args); + return this.mix(...args); }); // Component accessors: extract scalar channels from a vec4 color const originalRed = fn.red; @@ -369,84 +369,80 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return p5.strandsNode(args[0]).w; }); - // Helper: RGB vec3 → HSB vec3 using backend-agnostic strands ops - const _rgb2hsb = (colorNode) => { - const r = colorNode.x; - const g = colorNode.y; - const b = colorNode.z; - // vec4 K = vec4(0, -1/3, 2/3, -1) - const K = fn.vec4(0.0, -1.0/3.0, 2.0/3.0, -1.0); - // p = mix(vec4(b, g, K.w, K.z), vec4(g, b, K.x, K.y), step(b, g)) - const p = fn.mix( - fn.vec4(b, g, K.w, K.z), - fn.vec4(g, b, K.x, K.y), - fn.step(b, g) - ); - // q = mix(vec4(p.x, p.y, p.w, r), vec4(r, p.y, p.z, p.x), step(p.x, r)) - const q = fn.mix( - fn.vec4(p.x, p.y, p.w, r), - fn.vec4(r, p.y, p.z, p.x), - fn.step(p.x, r) - ); - const d = q.x.sub(fn.min(q.w, q.y)); - const e = p5.strandsNode(1.0e-10); - const h = fn.abs(q.z.add(q.w.sub(q.y).div(d.mult(6.0).add(e)))); - const s = d.div(q.x.add(e)); - const v = q.x; - return fn.vec3(h, s, v); - } - - // Helper: RGB vec3 → HSL vec3 using backend-agnostic strands ops - const _rgb2hsl = (colorNode) => { - const r = colorNode.x; - const g = colorNode.y; - const b = colorNode.z; - const maxC = fn.max(r, fn.max(g, b)); - const minC = fn.min(r, fn.min(g, b)); - const l = maxC.add(minC).div(2.0); - const d = maxC.sub(minC); - const e = p5.strandsNode(1.0e-10); - const s = fn.mix( - p5.strandsNode(0.0), - d.div(p5.strandsNode(1.0).sub(fn.abs(l.mult(2.0).sub(1.0)))), - fn.step(e, d) - ); - // Hue: use mix+step to avoid conditionals - const h_rg = fn.mod(g.sub(b).div(d.add(e)), p5.strandsNode(6.0)).div(6.0); - const h_gb = b.sub(r).div(d.add(e)).add(2.0).div(6.0); - const h_br = r.sub(g).div(d.add(e)).add(4.0).div(6.0); - const isR = fn.step(maxC.sub(e), r).mult(fn.step(r.sub(e), maxC)); - const isG = fn.step(maxC.sub(e), g).mult(fn.step(g.sub(e), maxC)); - const h = fn.mix(fn.mix(h_br, h_gb, isG), h_rg, isR); - return fn.vec3(h, s, l); - } - + // 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(fn.vec3(colorNode.x, colorNode.y, colorNode.z)).x; + 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(fn.vec3(colorNode.x, colorNode.y, colorNode.z)).y; + 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(fn.vec3(colorNode.x, colorNode.y, colorNode.z)).z; + 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(fn.vec3(colorNode.x, colorNode.y, colorNode.z)).z; + return _rgb2hsl(this, this.vec3(colorNode.x, colorNode.y, colorNode.z)).z; }); augmentFn(fn, p5, 'getTexture', function (...rawArgs) { From b5955f11cd608e5041af946e1d87be5cf131f4a3 Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Fri, 5 Jun 2026 00:27:26 +0530 Subject: [PATCH 8/9] docs(color): add p5.strands behavior note to color() JSDoc --- src/color/creating_reading.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/color/creating_reading.js b/src/color/creating_reading.js index 7690104406..340887294d 100644 --- a/src/color/creating_reading.js +++ b/src/color/creating_reading.js @@ -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. From 2004c69554be424f130395cf6d748cd5454cee50 Mon Sep 17 00:00:00 2001 From: LalitNarayanYadav Date: Tue, 9 Jun 2026 16:03:45 +0530 Subject: [PATCH 9/9] add test file --- test/unit/webgl/p5.Shader.js | 104 +++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 786540cafa..470bd9fb51 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -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);