p5
Version:
[](https://www.npmjs.com/package/p5)
1,219 lines (1,121 loc) • 48 kB
JavaScript
import { u as unaryOpNode, c as createStrandsNode, S as StrandsNode, p as primitiveConstructorNode, v as variableNode, a as structInstanceNode, b as structConstructorNode, d as binaryOpNode, e as statementNode, f as functionCallNode } from '../ir_builders-CMXkjMoV.js';
import { OperatorTable, StatementType, NodeType, DataType, BaseType, BlockType, isStructType, structType, HOOK_PARAM_PREFIX } from './ir_types.js';
import { strandsBuiltinFunctions } from './strands_builtins.js';
import { StrandsConditional } from './strands_conditionals.js';
import { StrandsFor } from './strands_for.js';
import { buildTernary } from './strands_ternary.js';
import { createBasicBlock, addEdge, pushBlock, recordInBasicBlock, popBlock } from './ir_cfg.js';
import { createNodeData, getOrCreateNode, getNodeDataFromID } from './ir_dag.js';
import { userError, dimensionMismatchError } from './strands_FES.js';
import './strands_phi_utils.js';
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 },
};
function _getBuiltinGlobalsCache(strandsContext) {
if (!strandsContext._builtinGlobals || strandsContext._builtinGlobals.dag !== strandsContext.dag) {
strandsContext._builtinGlobals = {
dag: strandsContext.dag,
nodes: new Map(),
uniformsAdded: new Set(),
};
}
// return the cache
return strandsContext._builtinGlobals
}
function getOrCreateUniformNode(strandsContext, uniformName, typeInfo, defaultValueFn) {
const cache = _getBuiltinGlobalsCache(strandsContext);
const cached = cache.nodes.get(uniformName);
if (cached) return cached;
if (!cache.uniformsAdded.has(uniformName)) {
cache.uniformsAdded.add(uniformName);
strandsContext.uniforms.push({
name: uniformName,
typeInfo,
defaultValue: defaultValueFn,
});
}
const { id, dimension } = variableNode(strandsContext, typeInfo, uniformName);
const node = createStrandsNode(id, dimension, strandsContext);
cache.nodes.set(uniformName, node);
return node;
}
function getBuiltinGlobalNode(strandsContext, name) {
const spec = BUILTIN_GLOBAL_SPECS[name];
if (!spec) return null;
const uniformName = `_p5_global_${name}`;
const instance = strandsContext.renderer?._pInst || strandsContext.p5?.instance;
const node = getOrCreateUniformNode(
strandsContext,
uniformName,
spec.typeInfo,
() => {
return instance ? spec.get(instance) : undefined;
}
);
node._originalBuiltinName = name;
return node;
}
function installBuiltinGlobalAccessors(strandsContext) {
if (strandsContext._builtinGlobalsAccessorsInstalled) return
const getRuntimeP5Instance = () => strandsContext.renderer?._pInst || strandsContext.p5?.instance;
for (const name of Object.keys(BUILTIN_GLOBAL_SPECS)) {
const spec = BUILTIN_GLOBAL_SPECS[name];
const backingKey = `_strands_${name}`;
// Define on window for global mode only
const inst = getRuntimeP5Instance();
if (inst?._isGlobal) {
Object.defineProperty(window, name, {
get: () => {
if (strandsContext.active) {
return getBuiltinGlobalNode(strandsContext, name);
}
const inst = getRuntimeP5Instance();
return spec.get(inst);
},
configurable: true,
});
}
// Capture original descriptor (held in closure for the getter to delegate to)
const originalProtoDesc = Object.getOwnPropertyDescriptor(strandsContext.p5.prototype, name);
// Define on p5.prototype for instance mode
Object.defineProperty(strandsContext.p5.prototype, name, {
get: function() {
if (strandsContext.active) {
return getBuiltinGlobalNode(strandsContext, name);
}
// If our setter stored a value on this instance, return it
if (Object.prototype.hasOwnProperty.call(this, backingKey)) {
return this[backingKey];
}
// Delegate to original getter (e.g. width -> this._renderer?.width)
if (originalProtoDesc?.get) {
return originalProtoDesc.get.call(this);
}
// Fall back to original value for data properties (like mouseX)
return originalProtoDesc?.value;
},
set: function(val) {
this[backingKey] = val;
},
configurable: true,
});
// Define on p5.Graphics.prototype for graphics mode
const GraphicsProto = strandsContext.p5?.Graphics?.prototype;
if (GraphicsProto) {
const originalDesc = Object.getOwnPropertyDescriptor(GraphicsProto, name);
Object.defineProperty(GraphicsProto, name, {
get: function() {
if (strandsContext.active) {
return getBuiltinGlobalNode(strandsContext, name);
}
// Delegate to original getter if it exists (class-level getters like width, deltaTime)
if (originalDesc?.get) {
return originalDesc.get.call(this);
}
return this[backingKey];
},
set: function(val) {
if (originalDesc?.set) {
originalDesc.set.call(this, val);
} else {
this[backingKey] = val;
}
},
configurable: true,
});
}
}
strandsContext._builtinGlobalsAccessorsInstalled = true;
}
function installInstanceIndexAccessor(strandsContext) {
if (strandsContext._instanceIndexAccessorInstalled) return;
const getRuntimeP5Instance = () => strandsContext.renderer?._pInst || strandsContext.p5?.instance;
const instanceIndexGetter = function() {
if (strandsContext.active) {
const node = variableNode(strandsContext, { baseType: BaseType.INT, dimension: 1 }, strandsContext.backend.instanceIdReference());
return createStrandsNode(node.id, node.dimension, strandsContext);
}
return undefined;
};
const inst = getRuntimeP5Instance();
if (inst?._isGlobal) {
Object.defineProperty(window, 'instanceIndex', {
get: instanceIndexGetter,
configurable: true,
});
}
Object.defineProperty(strandsContext.p5.prototype, 'instanceIndex', {
get: instanceIndexGetter,
configurable: true,
});
const GraphicsProto = strandsContext.p5?.Graphics?.prototype;
if (GraphicsProto) {
Object.defineProperty(GraphicsProto, 'instanceIndex', {
get: instanceIndexGetter,
configurable: true,
});
}
strandsContext._instanceIndexAccessorInstalled = true;
}
//////////////////////////////////////////////
// Prototype mirroring helpers
//////////////////////////////////////////////
/*
* Permanently augment both p5.prototype (fn) and p5.Graphics.prototype
* with a strands function. Overwrites unconditionally - strands wrappers
* are the correct dual mode implementation.
*/
function augmentFn(fn, p5, name, value) {
fn[name] = value;
const GraphicsProto = p5?.Graphics?.prototype;
if (GraphicsProto) {
GraphicsProto[name] = value;
}
}
/*
* Temporarily augment window, p5.prototype (fn), and p5.Graphics.prototype
* with a hook function. Saves previous values into strandsContext override
* stores so deinitStrandsContext can restore them.
*/
function augmentFnTemporary(fn, strandsContext, name, value) {
strandsContext.windowOverrides[name] = window[name];
strandsContext.fnOverrides[name] = fn[name];
window[name] = value;
fn[name] = value;
const GraphicsProto = strandsContext.p5?.Graphics?.prototype;
if (GraphicsProto) {
strandsContext.graphicsOverrides[name] = Object.prototype.hasOwnProperty.call(GraphicsProto, name)
? GraphicsProto[name]
: undefined;
GraphicsProto[name] = value;
}
}
//////////////////////////////////////////////
// User nodes
//////////////////////////////////////////////
function initGlobalStrandsAPI(p5, fn, strandsContext) {
// We augment the strands node with operations programatically
// this means methods like .add, .sub, etc can be chained
for (const { name, arity, opCode } of OperatorTable) {
if (arity === 'binary') {
StrandsNode.prototype[name] = function (...right) {
const { id, dimension } = binaryOpNode(strandsContext, this, right, opCode);
return createStrandsNode(id, dimension, strandsContext);
};
}
if (arity === 'unary') {
p5[name] = function (nodeOrValue) {
const { id, dimension } = unaryOpNode(strandsContext, nodeOrValue, opCode);
return createStrandsNode(id, dimension, strandsContext);
};
}
}
//////////////////////////////////////////////
// Unique Functions
//////////////////////////////////////////////
augmentFn(fn, p5, 'discard', function() {
statementNode(strandsContext, StatementType.DISCARD);
});
augmentFn(fn, p5, 'break', function() {
statementNode(strandsContext, StatementType.BREAK);
});
p5.break = fn.break;
augmentFn(fn, p5, 'instanceID', function() {
const node = variableNode(strandsContext, { baseType: BaseType.INT, dimension: 1 }, strandsContext.backend.instanceIdReference());
return createStrandsNode(node.id, node.dimension, strandsContext);
});
// Internal methods use p5 static methods; user-facing methods use fn.
// 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();
};
augmentFn(fn, p5, 'strandsFor', p5.strandsFor);
p5.strandsTernary = function(condition, ifTrue, ifFalse) {
return buildTernary(strandsContext, condition, ifTrue, ifFalse);
};
augmentFn(fn, p5, 'strandsTernary', p5.strandsTernary);
p5.strandsEarlyReturn = function(value) {
const { dag, cfg } = strandsContext;
// Ensure we're inside a hook
if (!strandsContext.activeHook) {
throw new Error('strandsEarlyReturn can only be used inside a hook callback');
}
// Convert value to a StrandsNode if it isn't already
const valueNode = value?.isStrandsNode ? value : p5.strandsNode(value);
// Create a new CFG block for the early return
const earlyReturnBlockID = createBasicBlock(cfg, BlockType.DEFAULT);
addEdge(cfg, cfg.currentBlock, earlyReturnBlockID);
pushBlock(cfg, earlyReturnBlockID);
// Create the early return statement node
const nodeData = createNodeData({
nodeType: NodeType.STATEMENT,
statementType: StatementType.EARLY_RETURN,
dependsOn: value !== undefined ? [valueNode.id] : []
});
const earlyReturnID = getOrCreateNode(dag, nodeData);
recordInBasicBlock(cfg, cfg.currentBlock, earlyReturnID);
// Add the value to the hook's earlyReturns array for later type checking
strandsContext.activeHook.earlyReturns.push({ earlyReturnID, valueNode });
popBlock(cfg);
return valueNode;
};
augmentFn(fn, p5, 'strandsEarlyReturn', p5.strandsEarlyReturn);
p5.strandsNode = function(...args) {
if (args.length === 1 && args[0] instanceof StrandsNode) {
return args[0];
}
if (args.length > 4) {
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();
const definedArgs = flatArgs.filter(a => a !== undefined && a !== null);
// If all args are undefined, this is likely a `let myVar` at the
// start of an if statement and it will be assigned within the branches.
// For that, we use an assign-on-use node, meaning we'll take the type of the
// values assigned to it.
if (definedArgs.length === 0) {
const { id, dimension } = primitiveConstructorNode(strandsContext, { baseType: BaseType.ASSIGN_ON_USE, dimension: null }, [0]);
return createStrandsNode(id, dimension, strandsContext);
}
const { id, dimension } = primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, definedArgs);
return createStrandsNode(id, dimension, strandsContext);//new StrandsNode(id, dimension, strandsContext);
};
//////////////////////////////////////////////
// Builtins, uniforms, variable constructors
//////////////////////////////////////////////
for (const [functionName, overrides] of Object.entries(strandsBuiltinFunctions)) {
const isp5Function = overrides[0].isp5Function;
if (isp5Function) {
const originalFn = fn[functionName];
augmentFn(fn, p5, functionName, function(...args) {
if (strandsContext.active) {
const { id, dimension } = functionCallNode(strandsContext, functionName, args);
return createStrandsNode(id, dimension, strandsContext);
} else {
return originalFn.apply(this, args);
}
});
} else {
augmentFn(fn, p5, functionName, function (...args) {
if (strandsContext.active) {
const { id, dimension } = functionCallNode(strandsContext, functionName, args);
return createStrandsNode(id, dimension, strandsContext);
} else {
p5._friendlyError(
`It looks like you've called ${functionName} outside of a shader's modify() function.`
);
}
});
}
}
// Alias lerp to GLSL mix in strands context
const originalLerp = fn.lerp;
augmentFn(fn, p5, 'lerp', function (...args) {
if (strandsContext.active) {
return this.mix(...args);
} else {
return originalLerp.apply(this, args);
}
});
const originalMap = fn.map;
augmentFn(fn, p5, 'map', function (...args) {
if (!strandsContext.active) {
return originalMap.apply(this, args);
}
const [n, start1, stop1, start2, stop2, withinBounds] = args;
const nNode = p5.strandsNode(n);
const start1Node = p5.strandsNode(start1);
const stop1Node = p5.strandsNode(stop1);
const t = nNode.sub(start1Node).div(stop1Node.sub(start1Node));
const result = this.mix(start2, stop2, t);
if (withinBounds) {
return this.clamp(result, this.min(start2, stop2), this.max(start2, stop2));
}
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 } = primitiveConstructorNode(
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);
return createStrandsNode(id, dimension, strandsContext);
} else {
p5._friendlyError(
`It looks like you've called getTexture outside of a shader's modify() function.`
);
}
});
// Add texture function as alias for getTexture with p5 fallback
const originalTexture = fn.texture;
augmentFn(fn, p5, 'texture', function (...args) {
if (strandsContext.active) {
return this.getTexture(...args);
} else {
return originalTexture.apply(this, args);
}
});
// Add noise function with backend-agnostic implementation
const originalNoise = fn.noise;
const originalNoiseDetail = fn.noiseDetail;
const originalRandom = fn.random;
const originalRandomGaussian=fn.randomGaussian;
const originalRandomSeed = fn.randomSeed;
const originalMillis = fn.millis;
strandsContext._noiseOctaves = null;
strandsContext._noiseAmpFalloff = null;
augmentFn(fn, p5, 'noiseDetail', function (lod, falloff = 0.5) {
if (!strandsContext.active) {
return originalNoiseDetail.apply(this, arguments);
}
strandsContext._noiseOctaves = lod;
strandsContext._noiseAmpFalloff = falloff;
});
augmentFn(fn, p5, 'noise', function (...args) {
if (!strandsContext.active) {
return originalNoise.apply(this, args); // fallback to regular p5.js noise
}
// Get noise shader snippet from the current renderer
const noiseSnippet = strandsContext.backend.getNoiseShaderSnippet();
strandsContext.vertexDeclarations.add(noiseSnippet);
strandsContext.fragmentDeclarations.add(noiseSnippet);
strandsContext.computeDeclarations.add(noiseSnippet);
// Make each input into a strands node so that we can check their dimensions
const strandsArgs = args.flat().map(arg => p5.strandsNode(arg));
let nodeArgs;
if (strandsArgs.length === 3) {
nodeArgs = [fn.vec3(strandsArgs[0], strandsArgs[1], strandsArgs[2])];
} else if (strandsArgs.length === 2) {
nodeArgs = [fn.vec3(strandsArgs[0], strandsArgs[1], 0)];
} else if (strandsArgs.length === 1 && strandsArgs[0].dimension <= 3) {
if (strandsArgs[0].dimension === 3) {
nodeArgs = strandsArgs;
} else if (strandsArgs[0].dimension === 2) {
nodeArgs = [fn.vec3(strandsArgs[0], 0)];
} else {
nodeArgs = [fn.vec3(strandsArgs[0], 0, 0)];
}
} else {
p5._friendlyError(
`It looks like you've called noise() with ${args.length} arguments. It only supports 1D to 3D input.`
);
}
const octaves = strandsContext._noiseOctaves !== null
? strandsContext._noiseOctaves
: fn._getNoiseOctaves();
const falloff = strandsContext._noiseAmpFalloff !== null
? strandsContext._noiseAmpFalloff
: fn._getNoiseAmpFalloff();
nodeArgs.push(octaves);
nodeArgs.push(falloff);
const { id, dimension } = functionCallNode(strandsContext, 'noise', nodeArgs, {
overloads: [{
params: [DataType.float3, DataType.int1, DataType.float1],
returnType: DataType.float1,
}]
});
return createStrandsNode(id, dimension, strandsContext);
});
strandsContext._randomSeed = null;
augmentFn(fn, p5, 'randomSeed', function (seed) {
if (!strandsContext.active) {
return originalRandomSeed.apply(this, arguments);
}
strandsContext._randomSeed = seed;
});
augmentFn(fn, p5, 'random', function (...args) {
if (!strandsContext.active) {
return originalRandom.apply(this, args);
}
const randomVertSnippet = strandsContext.backend.getRandomVertexShaderSnippet();
const randomFragSnippet = strandsContext.backend.getRandomFragmentShaderSnippet();
strandsContext.vertexDeclarations.add(randomVertSnippet);
strandsContext.fragmentDeclarations.add(randomFragSnippet);
if (strandsContext.backend.getRandomComputeShaderSnippet) {
const randomComputeSnippet = strandsContext.backend.getRandomComputeShaderSnippet();
strandsContext.computeDeclarations.add(randomComputeSnippet);
}
let seedNode;
if (strandsContext._randomSeed !== null && strandsContext._randomSeed.isStrandsNode) {
seedNode = strandsContext._randomSeed;
} else {
const userSeed = strandsContext._randomSeed;
seedNode = getOrCreateUniformNode(
strandsContext,
'_p5_randomSeed',
DataType.float1,
userSeed !== null
? () => userSeed
: () => performance.now(),
);
}
// The shader-side random() owns a private per-invocation counter, so a
// single AST node still produces distinct values across runtime loop
// iterations. We just pass the seed.
const nodeArgs = [seedNode];
const randomOverloads = [{
params: [DataType.float1],
returnType: DataType.float1,
}];
if (args.length === 0) {
const { id, dimension } = functionCallNode(strandsContext, 'random', nodeArgs, {
overloads: randomOverloads,
});
return createStrandsNode(id, dimension, strandsContext);
} else if (args.length === 1) {
// random(max) → [0, max)
const rawNode = functionCallNode(strandsContext, 'random', nodeArgs, {
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 = functionCallNode(strandsContext, 'random', nodeArgs, {
overloads: randomOverloads,
});
const rawStrandsNode = createStrandsNode(rawNode.id, rawNode.dimension, strandsContext);
const minNode = p5.strandsNode(args[0]);
const maxNode = p5.strandsNode(args[1]);
// min + raw * (max - min)
return rawStrandsNode.mult(maxNode.sub(minNode)).add(minNode);
} else {
p5._friendlyError(
`It looks like you've called random() with ${args.length} arguments. In strands, random() supports 0, 1, or 2 numeric arguments.`
);
}
});
augmentFn(fn, p5, 'randomGaussian', function(...args){
if(!strandsContext.active){
return originalRandomGaussian.apply(this, args);
}
const mean = args.length >= 1 ? args[0] : 0;
const stdDev = args.length >= 2 ? args[1] : 1;
const u1 = this.max(this.random(), 1e-6);
const u2 = this.random();
const z = this.sqrt(this.log(u1).mult(-2)).mult(this.cos(u2.mult(2*Math.PI)));
return z.mult(stdDev).add(mean);
});
augmentFn(fn, p5, 'millis', function (...args) {
if (!strandsContext.active) {
return originalMillis.apply(this, args);
}
const instance = strandsContext.renderer?._pInst || strandsContext.p5?.instance;
return getOrCreateUniformNode(
strandsContext,
'_p5_global_millis',
DataType.float1,
() => {
return instance ? instance.millis() : undefined;
}
);
});
// Next is type constructors and uniform functions.
// For some of them, we have aliases so that you can write either a more human-readable
// variant or also one more directly translated from GLSL, or to be more compatible with
// APIs we documented at the release of 2.x and have to continue supporting.
for (const type in DataType) {
if (type === BaseType.DEFER || type === BaseType.ASSIGN_ON_USE || type === 'sampler') {
continue;
}
const typeInfo = DataType[type];
const typeAliases = [];
let pascalTypeName;
if (/^[ib]vec/.test(typeInfo.fnName)) {
pascalTypeName = typeInfo.fnName
.slice(0, 2).toUpperCase()
+ typeInfo.fnName
.slice(2)
.toLowerCase();
typeAliases.push(pascalTypeName.replace('Vec', 'Vector'));
} else {
pascalTypeName = typeInfo.fnName.charAt(0).toUpperCase()
+ typeInfo.fnName.slice(1);
if (pascalTypeName === 'Sampler2D') {
typeAliases.push('Texture');
} else if (/^vec/.test(typeInfo.fnName)) {
typeAliases.push(pascalTypeName.replace('Vec', 'Vector'));
}
}
augmentFn(fn, p5, `uniform${pascalTypeName}`, function(name, defaultValue) {
const { id, dimension } = variableNode(strandsContext, typeInfo, name);
strandsContext.uniforms.push({ name, typeInfo, defaultValue });
return createStrandsNode(id, dimension, strandsContext);
});
// Shared variables with smart context detection
augmentFn(fn, p5, `shared${pascalTypeName}`, function(name) {
const { id, dimension } = variableNode(strandsContext, typeInfo, name);
// Initialize shared variables tracking if not present
if (!strandsContext.sharedVariables) {
strandsContext.sharedVariables = new Map();
}
// Track this shared variable for smart declaration generation
strandsContext.sharedVariables.set(name, {
typeInfo,
usedInVertex: false,
usedInFragment: false,
});
return createStrandsNode(id, dimension, strandsContext);
});
// Alias varying* as shared* for backward compatibility
augmentFn(fn, p5, `varying${pascalTypeName}`, fn[`shared${pascalTypeName}`]);
for (const typeAlias of typeAliases) {
// For compatibility, also alias uniformVec2 as uniformVector2, what we initially
// documented these as
augmentFn(fn, p5, `uniform${typeAlias}`, fn[`uniform${pascalTypeName}`]);
augmentFn(fn, p5, `varying${typeAlias}`, fn[`varying${pascalTypeName}`]);
augmentFn(fn, p5, `shared${typeAlias}`, fn[`shared${pascalTypeName}`]);
}
const originalp5Fn = fn[typeInfo.fnName];
augmentFn(fn, p5, typeInfo.fnName, function(...args) {
if (strandsContext.active) {
if (args.length === 1 && args[0].dimension && args[0].dimension === typeInfo.dimension) {
const { id, dimension } = functionCallNode(
strandsContext,
strandsContext.backend.getTypeName(typeInfo.baseType, typeInfo.dimension),
args,
{
overloads: [{
params: [args[0].typeInfo()],
returnType: typeInfo,
}]
}
);
return createStrandsNode(id, dimension, strandsContext);
} else {
// For vector types with a single argument, repeat it for each component
if (typeInfo.dimension > 1 && args.length === 1 && !Array.isArray(args[0]) &&
!(args[0] instanceof StrandsNode && args[0].dimension > 1) &&
(typeInfo.baseType === BaseType.FLOAT || typeInfo.baseType === BaseType.INT || typeInfo.baseType === BaseType.BOOL)) {
args = Array(typeInfo.dimension).fill(args[0]);
}
const { id, dimension } = primitiveConstructorNode(strandsContext, typeInfo, args);
return createStrandsNode(id, dimension, strandsContext);
}
} else if (originalp5Fn) {
return originalp5Fn.apply(this, args);
} else {
p5._friendlyError(
`It looks like you've called ${typeInfo.fnName} outside of a shader's modify() function.`
);
}
});
}
// Storage buffer uniform function for compute shaders
fn.uniformStorage = function(name, bufferOrSchema) {
let schema = null;
let defaultValue = null;
// If it's a function, evaluate it immediately to infer schema,
// then store the function so it gets called each frame.
let value = bufferOrSchema;
if (typeof bufferOrSchema === 'function') {
value = bufferOrSchema();
if (value?._schema) {
defaultValue = bufferOrSchema;
}
}
if (value?._schema) {
// Struct storage buffer with pre-computed schema
schema = value._schema;
if (defaultValue === null) defaultValue = value;
} else if (value && typeof value === 'object' && !value._isStorageBuffer) {
// Plain object schema template -- only used to infer struct layout, not as a default value
schema = strandsContext.renderer?._inferStructSchema(value) ?? null;
} else if (value?._isStorageBuffer) {
defaultValue = bufferOrSchema;
}
const { id, dimension } = variableNode(
strandsContext,
{ baseType: 'storage', dimension: 1 },
name
);
strandsContext.uniforms.push({
name,
typeInfo: { baseType: 'storage', dimension: 1, schema },
defaultValue,
});
// Create StrandsNode with _originalIdentifier set (like varying variables)
// This enables proper assignment node creation and ordering preservation
const node = createStrandsNode(id, dimension, strandsContext);
node._originalIdentifier = name;
node._originalBaseType = 'storage';
node._originalDimension = 1;
node._schema = schema;
return node;
};
}
//////////////////////////////////////////////
// Per-Hook functions
//////////////////////////////////////////////
function createHookArguments(strandsContext, parameters){
const args = [];
const dag = strandsContext.dag;
for (const param of parameters) {
if(isStructType(param.type)) {
const structTypeInfo = structType(param);
const { id, dimension } = structInstanceNode(strandsContext, structTypeInfo, `${HOOK_PARAM_PREFIX}${param.name}`, []);
const structNode = createStrandsNode(id, dimension, strandsContext).withStructProperties(
structTypeInfo.properties.map(prop => prop.name)
);
for (let i = 0; i < structTypeInfo.properties.length; i++) {
const propertyType = structTypeInfo.properties[i];
Object.defineProperty(structNode, propertyType.name, {
get() {
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;
const rebuilt = structInstanceNode(strandsContext, structTypeInfo, `${HOOK_PARAM_PREFIX}${param.name}`, newDeps);
structNode.id = rebuilt.id;
};
// TODO: implement member access operations
// const { id, components } = createMemberAccessNode(strandsContext, structNode, componentNodes[i], componentTypeInfo.dataType);
// const memberAccessNode = new StrandsNode(id, components);
// return memberAccessNode;
return createStrandsNode(propNode.id, propNode.dimension, strandsContext, onRebind);
},
set(val) {
const valDim = val?.isStrandsNode
? val.dimension
: (Array.isArray(val) ? val.length : 1);
if( valDim !== propertyType.dataType.dimension && valDim !== 1){
dimensionMismatchError(
propertyType.dataType.dimension,
valDim,
`${param.name}.${propertyType.name}`
);
}
const oldDependsOn = dag.dependsOn[structNode.id];
const newDependsOn = [...oldDependsOn];
let newValueID;
if (val?.isStrandsNode) {
newValueID = val.id;
}
else {
let newVal = primitiveConstructorNode(strandsContext, propertyType.dataType, val);
newValueID = newVal.id;
}
newDependsOn[i] = newValueID;
const newStructInfo = structInstanceNode(strandsContext, structTypeInfo, `${HOOK_PARAM_PREFIX}${param.name}`, newDependsOn);
structNode.id = newStructInfo.id;
}
});
}
args.push(structNode);
}
else /*if(isNativeType(paramType.typeName))*/ {
// Skip sampler parameters - they don't need strands nodes
if (param.type.typeName === 'sampler') {
continue;
}
if (!param.type.dataType) {
throw new Error(`Missing dataType for parameter ${param.name} of type ${param.type.typeName}`);
}
const typeInfo = param.type.dataType;
const { id, dimension } = variableNode(strandsContext, typeInfo, `${HOOK_PARAM_PREFIX}${param.name}`);
const arg = createStrandsNode(id, dimension, strandsContext);
args.push(arg);
}
}
return args;
}
function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName) {
if (!(returned?.isStrandsNode)) {
// try {
const result = 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}`
// );
// }
}
const dag = strandsContext.dag;
let returnedNodeID = returned.id;
const receivedType = {
baseType: dag.baseTypes[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;
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.`
);
}
else {
const result = primitiveConstructorNode(strandsContext, expectedType, returned);
returnedNodeID = result.id;
}
}
else if (receivedType.baseType !== expectedType.baseType) {
const result = primitiveConstructorNode(strandsContext, expectedType, returned);
returnedNodeID = result.id;
}
return returnedNodeID;
}
function createShaderHooksFunctions(strandsContext, fn, shader) {
installBuiltinGlobalAccessors(strandsContext);
installInstanceIndexAccessor(strandsContext);
// Add shader context to hooks before spreading
const vertexHooksWithContext = Object.fromEntries(
Object.entries(shader.hooks.vertex).map(([name, hook]) => [name, { ...hook, shaderContext: 'vertex' }])
);
const fragmentHooksWithContext = Object.fromEntries(
Object.entries(shader.hooks.fragment).map(([name, hook]) => [name, { ...hook, shaderContext: 'fragment' }])
);
const computeHooksWithContext = Object.fromEntries(
Object.entries(shader.hooks.compute).map(([name, hook]) => [name, { ...hook, shaderContext: 'compute' }])
);
const availableHooks = {
...vertexHooksWithContext,
...fragmentHooksWithContext,
...computeHooksWithContext,
};
const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name));
const { cfg, dag } = strandsContext;
for (const hookType of hookTypes) {
const hook = function(hookUserCallback) {
const args = setupHook();
hook._result = hookUserCallback(...args) ?? hook._result;
finishHook();
};
// In the flat strands API, this is how result-returning hooks
// are used
hook.set = function(result) {
hook._result = result;
};
hook._active = false;
const numStructArgs = hookType.parameters.filter(
param => param.type && param.type.properties
).length;
let argIdx = -1;
if (numStructArgs === 1) {
argIdx = hookType.parameters.findIndex(
param => param.type && param.type.properties
);
}
if (argIdx >= 0) {
const structParam = hookType.parameters[argIdx];
if (structParam.type.properties) {
const nameMatch = /^get([A-Z0-9]\w*)$/.exec(hookType.name);
const displayName = nameMatch
? nameMatch[1][0].toLowerCase() + nameMatch[1].slice(1)
: hookType.name;
for (const prop of structParam.type.properties) {
const key = prop.name;
Object.defineProperty(hook, key, {
get() {
if (!this._active) {
userError(
'scope error',
`It looks like you're trying to access "${displayName}.${key}" outside of its begin()/end() block.\n\n` +
`Properties of ${displayName} are only available between ` +
`${displayName}.begin() and ${displayName}.end().\n\n` +
`To share data between hooks, use sharedVec3() or sharedFloat() ` +
`to pass values between them.`
);
}
return this._args[this._argIdx][key];
},
set(val) {
if (!this._active) {
userError(
'scope error',
`It looks like you're trying to set "${displayName}.${key}" outside of its begin()/end() block.`
);
}
this._args[this._argIdx][key] = val;
},
enumerable: true,
});
}
}
}
let entryBlockID;
function setupHook() {
strandsContext.activeHook = hook;
entryBlockID = createBasicBlock(cfg, BlockType.FUNCTION);
addEdge(cfg, cfg.currentBlock, entryBlockID);
pushBlock(cfg, entryBlockID);
const args = createHookArguments(strandsContext, hookType.parameters);
hook._active = true;
hook._args = args;
hook._argIdx = argIdx;
hook._properties = [];
for (let i = 0; i < args.length; i++) {
if (i === argIdx) {
for (const key of args[argIdx].structProperties || []) {
hook._properties.push(key);
}
if (hookType.returnType?.typeName === hookType.parameters[argIdx].type.typeName) {
hook.set(args[argIdx]);
}
} else {
hook._properties.push(hookType.parameters[i].name);
hook[hookType.parameters[i].name] = args[i];
}
}
return args;
}
function finishHook() {
hook._active = false;
const userReturned = hook._result;
strandsContext.activeHook = undefined;
const expectedReturnType = hookType.returnType;
let rootNodeID = null;
const handleRetVal = (retNode) => {
if(isStructType(expectedReturnType)) {
const expectedStructType = structType(expectedReturnType);
if (retNode?.isStrandsNode) {
const returnedNode = getNodeDataFromID(strandsContext.dag, retNode.id);
if (returnedNode.baseType !== expectedStructType.typeName) {
const receivedTypeName = returnedNode.baseType || 'undefined';
const receivedDim = dag.dimensions[retNode.id];
const receivedTypeDisplay = receivedDim > 1 ?
`${receivedTypeName}${receivedDim}` : receivedTypeName;
const expectedProps = expectedStructType.properties
.map(p => p.name).join(', ');
userError('type error',
`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` +
`${hookType.name}((inputs) => {\n` +
` // Modify properties of inputs\n` +
` inputs.someProperty = ...;\n` +
` return inputs; // Return the modified struct\n` +
`})`
);
}
const newDeps = returnedNode.dependsOn.slice();
for (let i = 0; i < expectedStructType.properties.length; i++) {
const expectedType = expectedStructType.properties[i].dataType;
const receivedNode = createStrandsNode(returnedNode.dependsOn[i], dag.dependsOn[retNode.id], strandsContext);
newDeps[i] = enforceReturnTypeMatch(strandsContext, expectedType, receivedNode, hookType.name);
}
dag.dependsOn[retNode.id] = newDeps;
return retNode.id;
}
else {
const expectedProperties = expectedStructType.properties;
const newStructDependencies = [];
for (let i = 0; i < expectedProperties.length; i++) {
const expectedProp = expectedProperties[i];
const propName = expectedProp.name;
const receivedValue = retNode[propName];
if (receivedValue === undefined) {
const expectedProps = expectedReturnType.properties.map(p => p.name).join(', ');
const receivedProps = Object.keys(retNode).join(', ');
userError('type error',
`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.`
);
}
const expectedTypeInfo = expectedProp.dataType;
const returnedPropID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, receivedValue, hookType.name);
newStructDependencies.push(returnedPropID);
}
const newStruct = structConstructorNode(strandsContext, expectedStructType, newStructDependencies);
return newStruct.id;
}
}
else if (!expectedReturnType.dataType || expectedReturnType.typeName?.trim() === 'void') {
return null;
}
else /*if(isNativeType(expectedReturnType.typeName))*/ {
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) {
dag.dependsOn[earlyReturnID] = [id];
} else {
dag.dependsOn[earlyReturnID] = [];
}
}
rootNodeID = userReturned ? handleRetVal(userReturned) : undefined;
const fullHookName = `${hookType.returnType.typeName} ${hookType.name}`;
const hookInfo = availableHooks[fullHookName];
strandsContext.hooks.push({
hookType,
entryBlockID,
rootNodeID,
shaderContext: hookInfo?.shaderContext, // 'vertex', 'fragment', or 'compute'
});
popBlock(cfg);
} hook.begin = setupHook;
hook.end = finishHook;
const aliases = [hookType.name];
if (strandsContext.baseShader?.hooks?.hookAliases?.[hookType.name]) {
aliases.push(...strandsContext.baseShader.hooks.hookAliases[hookType.name]);
}
// If the hook has a name like getPixelInputs, create an alias without
// the get* prefix, like pixelInputs
const nameMatch = /^get([A-Z0-9]\w*)$/.exec(hookType.name);
if (nameMatch) {
const unprefixedName = nameMatch[1][0].toLowerCase() + nameMatch[1].slice(1);
if (!fn[unprefixedName]) {
aliases.push(unprefixedName);
}
}
for (const name of aliases) {
augmentFnTemporary(fn, strandsContext, name, hook);
}
hook.earlyReturns = [];
}
}
export { createShaderHooksFunctions, initGlobalStrandsAPI };