Increasing access
I've been developing on the side an SDF renderer addon for p5. It currently looks something like this, as an API:
let mySDF
function setup() {
createCanvas(windowWidth, windowHeight, WEBGL)
mySDF = buildSDF(function() {
sdfScene.begin()
let sdf = distanceFunction(sdfScene)
sdf.smoothUnion(30)
sdf.push()
sdf.translate(100 + 50 * sin(millis() * 0.001), 0, 0)
sdf.sphere(40)
sdf.pop()
sdf.sphere(60)
// TODO deal with materials etc here somehow
sdfScene.dist = sdf.get()
sdfScene.end()
})
}
function draw() {
background(0)
orbitControl()
noStroke()
lights()
fill('red')
specularMaterial(200)
shininess(300)
mySDF.draw(200)
push()
fill('green')
const angle = millis() * 0.001
const r = 150
translate(r*cos(angle), 0, r*sin(angle))
box(50)
pop()
}
However, if statements and for loops do not work with it. e.g. this will not work:
for (let row = 0; row < 4; row++) {
for (let col = 0; col < cols; col++) {
sdf.push()
sdf.fill(68, 68, 74)
sdf.translate(kX0 + col * pitchX, kY, kZ0 + row * pitchZ)
sdf.roundBox(keyW, keyH, keyD, keyR)
sdf.pop()
}
}
However, this would:
let minDist = 10000
for (let row = 0; row < 4; row++) {
for (let col = 0; col < cols; col++) {
minDist = min(minDist, boxSDF(...))
}
}
To a lesser extent, my p5.env library also needs this: the current color is also hidden state: https://github.com/davepagurek/p5.env/blob/241ab1d5d13bc44ba8dda4563e159c841c824f50/p5.env.js#L400 But other aspects of this library do work with if/else etc because you can use if/else for methods that input to the ones linked above that use hidden state.
The reason is that strands needs to convert control flow into its internal graph representation of your program, and to do that, it needs to know what gets modified in each part of the control flow. In the minDist example, the state updates are explicit so strands knows about it but can handle it.
However, the implicit internal state updates in my initial library design more closely mirror how p5 APIs work. If we can find a way to let addon authors make strands APIs that look more like regular p5, then it can help make strands feel more familiar and more p5-y/magical.
While the main point of strands is not to be "magical", and is about creating teachable moments with enough scaffolding to keep people unblocked, it is also a way that we can let more shader functionality be exposed in a similar mental model to the rest of p5 to make artists more productive. This would be most useful for that latter use case.
Most appropriate sub-area of p5.js?
Feature enhancement details
The main approach I can think of is to let addons write a function that does explicitly return state, i.e it takes in the current state and returns a new one. In TypeScript terms:
type StateFunction<State> = (currentState: State, ...otherArgs: any[]) => State
...and then we mark the function in some way that would tell the transpiler to turn this, where these different state-updating functions look like they are reading some implicit internal state and return nothing:
for (let row = 0; row < 4; row++) {
for (let col = 0; col < cols; col++) {
push()
fill(68, 68, 74)
translate(kX0 + col * pitchX, kY, kZ0 + row * pitchZ)
roundBox(keyW, keyH, keyD, keyR)
pop()
}
}
...into this, where it's all explicit:
let state = initialState()
for (let row = 0; row < 4; row++) {
for (let col = 0; col < cols; col++) {
state = push(state)
state = fill(state, 68, 68, 74)
state = translate(state, kX0 + col * pitchX, kY, kZ0 + row * pitchZ)
state = roundBox(state, keyW, keyH, keyD, keyR)
state = pop(state)
}
}
Now this could look something like this, where a plugin registers a bunch of methods that operate on some state, with initialState being something special that we call at the start of a hook whenever we see one of these tagged methods used. API could look like this:
function myAddon(p5, fn) {
p5.registerStrandsState(
// State initializer
function initialState() {
return [{ minDist: 1000, fill: [255, 255, 255] }]
},
// Other methods mutating the state
{
push: (state) => [...state, {...state.at(-1)}],
pop: (state) => state.slice(0, -1),
fill: (state, r, g, b) => [...state, {...state.at(-1), fill: p5.strandsNode([r, g, b])}],
// etc
},
// Other methods that just read the state but then do something else
{
getDistance: (state) => state.at(-1).minDist
}
)
}
...however I imagine we maybe want push/pop to globally work across state that different addons may add, so we maybe would want that to be generalized. The plugin API could be something like this:
function myAddon(p5, fn) {
p5.registerStrandsState(
// State initializer
function initialState() {
return { fill: [255, 255, 255], minDist: 1000 }
},
'sdf', // unique key for this plugin's state
// Other methods using the state. no push/pop this time
{
fill: (state, r, g, b) => [...state, {...state.at(-1), fill: p5.strandsNode([r, g, b])}],
// etc
},
// Other methods that just read the state but then do something else
{
getDistance: (state) => state.minDist
}
)
}
...and then we auto-namespace addon state in the transpiled output:
let state = {}
state.sdf = __p5.strandsStateInitializers.sdf()
for (let row = 0; row < 4; row++) {
for (let col = 0; col < cols; col++) {
state = push(state) // Push and pop are still shared
state.sdf = fill(state.sdf, 68, 68, 74) // This is scoped though
state.sdf = translate(state.sdf, kX0 + col * pitchX, kY, kZ0 + row * pitchZ)
state.sdf = roundBox(state.sdf, keyW, keyH, keyD, keyR)
state = pop(state)
}
}
Registering methods would both add them to p5.prototype but also add some extra properties to those functions, like how in #8817 we're starting to add .argTypes as an optional property of a strands function to help with FES and to allow arrays to be used in function signatures without them all being converted to strands vectors. We could call it something like .stateMetadata and we can check in the transpiler if this exists, and update it if so.
Increasing access
I've been developing on the side an SDF renderer addon for p5. It currently looks something like this, as an API:
However, if statements and for loops do not work with it. e.g. this will not work:
However, this would:
To a lesser extent, my p5.env library also needs this: the current color is also hidden state: https://github.com/davepagurek/p5.env/blob/241ab1d5d13bc44ba8dda4563e159c841c824f50/p5.env.js#L400 But other aspects of this library do work with if/else etc because you can use if/else for methods that input to the ones linked above that use hidden state.
The reason is that strands needs to convert control flow into its internal graph representation of your program, and to do that, it needs to know what gets modified in each part of the control flow. In the
minDistexample, the state updates are explicit so strands knows about it but can handle it.However, the implicit internal state updates in my initial library design more closely mirror how p5 APIs work. If we can find a way to let addon authors make strands APIs that look more like regular p5, then it can help make strands feel more familiar and more p5-y/magical.
While the main point of strands is not to be "magical", and is about creating teachable moments with enough scaffolding to keep people unblocked, it is also a way that we can let more shader functionality be exposed in a similar mental model to the rest of p5 to make artists more productive. This would be most useful for that latter use case.
Most appropriate sub-area of p5.js?
Feature enhancement details
The main approach I can think of is to let addons write a function that does explicitly return state, i.e it takes in the current state and returns a new one. In TypeScript terms:
...and then we mark the function in some way that would tell the transpiler to turn this, where these different state-updating functions look like they are reading some implicit internal state and return nothing:
...into this, where it's all explicit:
Now this could look something like this, where a plugin registers a bunch of methods that operate on some state, with
initialStatebeing something special that we call at the start of a hook whenever we see one of these tagged methods used. API could look like this:...however I imagine we maybe want push/pop to globally work across state that different addons may add, so we maybe would want that to be generalized. The plugin API could be something like this:
...and then we auto-namespace addon state in the transpiled output:
Registering methods would both add them to
p5.prototypebut also add some extra properties to those functions, like how in #8817 we're starting to add.argTypesas an optional property of a strands function to help with FES and to allow arrays to be used in function signatures without them all being converted to strands vectors. We could call it something like.stateMetadataand we can check in the transpiler if this exists, and update it if so.