Documentation Index Fetch the complete documentation index at: https://mintlify.com/vaneenige/phenomenon/llms.txt
Use this file to discover all available pages before exploring further.
By default, attributes are calculated once when an instance is created. Dynamic attributes allow you to recalculate and update attribute data during runtime, enabling continuous, evolving particle animations.
Why use dynamic attributes
Dynamic attributes are essential when you need to:
Create looping animations without reversing direction
Update particle positions based on user interaction
Morph between multiple target positions
Implement particle physics simulations
Attribute calculations happen on the CPU, so use them judiciously with large particle counts. See performance considerations below.
Basic attribute updating
Use prepareAttribute() to recalculate an attribute with new data:
phenomenon . add ( 'particles' , {
attributes ,
multiplier: 4000 ,
vertex ,
fragment ,
uniforms ,
onRender : instance => {
const { uProgress } = instance . uniforms ;
uProgress . value += 0.01 ;
if ( uProgress . value >= 1 ) {
// Recalculate the end position attribute
const newEnd = {
x: getRandom ( 1 ),
y: getRandom ( 1 ),
z: getRandom ( 1 ),
};
instance . prepareAttribute ({
name: 'aPositionEnd' ,
data : () => [
newEnd . x + getRandom ( 0.1 ),
newEnd . y + getRandom ( 0.1 ),
newEnd . z + getRandom ( 0.1 ),
],
size: 3 ,
});
uProgress . value = 0 ;
}
},
});
prepareAttribute() recalculates data for all particles using the data function. This is CPU-intensive with high multipliers.
Efficient attribute switching
When you want to reuse existing data instead of recalculating it, use prepareBuffer() to directly update the buffer:
onRender : instance => {
const { uProgress } = instance . uniforms ;
uProgress . value += 0.01 ;
if ( uProgress . value >= 1 ) {
// Copy the end position data to start position
instance . prepareBuffer ({
name: 'aPositionStart' ,
data: instance . attributes [ 1 ]. data , // Reuse existing data
size: 3 ,
});
// Calculate new end position
instance . prepareAttribute ({
name: 'aPositionEnd' ,
data : () => [
newEnd . x + getRandom ( 0.1 ),
newEnd . y + getRandom ( 0.1 ),
newEnd . z + getRandom ( 0.1 ),
],
size: 3 ,
});
uProgress . value = 0 ;
}
}
prepareBuffer() only updates the GPU buffer with existing data—no recalculation happens. This is much faster than prepareAttribute() when you’re reusing data.
Understanding the difference
prepareAttribute()
Recalculates attribute data using the data function, then uploads to GPU. instance . prepareAttribute ({
name: 'aPositionEnd' ,
data : () => [ Math . random (), Math . random (), Math . random ()],
size: 3 ,
});
When to use:
You need new, calculated values
Data depends on particle index or multiplier
Implementing physics or procedural generation
prepareBuffer()
Uploads existing data directly to the GPU buffer without recalculation. instance . prepareBuffer ({
name: 'aPositionStart' ,
data: instance . attributes [ 1 ]. data ,
size: 3 ,
});
When to use:
Copying data from another attribute
Using pre-computed arrays
Swapping between predefined states
Real-world example: Continuous transitions
This example from the demo creates a continuous particle animation that seamlessly transitions between random positions:
const dynamicAttributes = true ;
const step = 0.01 ;
const duration = 0.6 ;
const multiplier = 4000 ;
const start = {
x: getRandom ( 1 ),
y: getRandom ( 1 ),
z: getRandom ( 1 ),
};
const end = {
x: getRandom ( 1 ),
y: getRandom ( 1 ),
z: getRandom ( 1 ),
};
const attributes = [
{
name: 'aPositionStart' ,
data : () => [
start . x + getRandom ( 0.1 ),
start . y + getRandom ( 0.1 ),
start . z + getRandom ( 0.1 )
],
size: 3 ,
},
{
name: 'aPositionEnd' ,
data : () => [
end . x + getRandom ( 0.1 ),
end . y + getRandom ( 0.1 ),
end . z + getRandom ( 0.1 )
],
size: 3 ,
},
];
const uniforms = {
uProgress: {
type: 'float' ,
value: 0.0 ,
},
};
let forward = true ;
phenomenon . add ( 'continuousParticles' , {
attributes ,
multiplier ,
vertex ,
fragment ,
uniforms ,
onRender : instance => {
const { uProgress } = instance . uniforms ;
uProgress . value += forward ? step : - step ;
if ( uProgress . value >= 1 ) {
if ( dynamicAttributes ) {
// Create new random end position
const newEnd = {
x: getRandom ( 1 ),
y: getRandom ( 1 ),
z: getRandom ( 1 ),
};
// Current end becomes new start (no recalculation)
instance . prepareBuffer ({
name: 'aPositionStart' ,
data: instance . attributes [ 1 ]. data ,
size: 3 ,
});
// Calculate new end position
instance . prepareAttribute ({
name: 'aPositionEnd' ,
data : () => [
newEnd . x + getRandom ( 0.1 ),
newEnd . y + getRandom ( 0.1 ),
newEnd . z + getRandom ( 0.1 ),
],
size: 3 ,
});
uProgress . value = 0 ;
} else {
forward = false ;
}
} else if ( uProgress . value <= 0 ) {
forward = true ;
}
},
});
This pattern creates seamless looping: when particles reach their destination, that position becomes the new starting point, and a new destination is generated. This prevents abrupt jumps or reversing animations.
Accessing attribute data
Attributes are stored in the instance’s attributes array. Each attribute contains:
{
name : 'aPositionEnd' , // Attribute name
size : 3 , // Number of values per particle
data : Float32Array ( ... ) // The actual data buffer
}
You can access attributes by their array index (based on order of creation) or by finding them:
// By index (if you know the order)
const endPositionData = instance . attributes [ 1 ]. data ;
// By name (more reliable)
const attributeIndex = instance . attributeKeys . indexOf ( 'aPositionEnd' );
const endPositionData = instance . attributes [ attributeIndex ]. data ;
The calculation of the data function happens on the CPU. Monitor your frame rate when using dynamic attributes with high multipliers.
Optimization strategies
Limit update frequency
Don’t update attributes every frame if you don’t need to: let frameCount = 0 ;
onRender : instance => {
frameCount ++ ;
// Only update every 10 frames
if ( frameCount % 10 === 0 && shouldUpdate ) {
instance . prepareAttribute ({
name: 'aPosition' ,
data : () => [ Math . random (), Math . random (), Math . random ()],
size: 3 ,
});
}
}
Use prepareBuffer() when possible
Reuse existing data instead of recalculating: // SLOW: Recalculates all particle data
instance . prepareAttribute ({
name: 'aPosition' ,
data : () => instance . attributes [ 0 ]. data ,
size: 3 ,
});
// FAST: Directly copies buffer
instance . prepareBuffer ({
name: 'aPosition' ,
data: instance . attributes [ 0 ]. data ,
size: 3 ,
});
Reduce multiplier
Lower multiplier values mean fewer particles to update: // May drop frames with dynamic attributes
multiplier : 100000
// More manageable for CPU calculations
multiplier : 10000
Pre-calculate when possible
Generate attribute states ahead of time and swap between them: // Pre-calculate multiple positions
const positions = Array . from ({ length: 5 }, () =>
new Float32Array ( multiplier * 3 ). map (() => Math . random ())
);
let currentPosition = 0 ;
onRender : instance => {
if ( shouldSwitch ) {
currentPosition = ( currentPosition + 1 ) % positions . length ;
instance . prepareBuffer ({
name: 'aPosition' ,
data: positions [ currentPosition ],
size: 3 ,
});
}
}
Monitor frame drops when using dynamic attributes:
let lastTime = performance . now ();
let frames = 0 ;
const phenomenon = new Phenomenon ({
settings: {
onRender : renderer => {
frames ++ ;
const now = performance . now ();
if ( now - lastTime >= 1000 ) {
console . log ( `FPS: ${ frames } ` );
frames = 0 ;
lastTime = now ;
}
},
},
});
Target 60 FPS for smooth animations. If you’re seeing drops below 30 FPS, reduce your multiplier or update frequency.
When to avoid dynamic attributes
Dynamic attributes are powerful but not always necessary. Consider these alternatives:
Complex patterns can often be achieved with shader calculations: // Instead of pre-calculating easing
attribute float aIndex ;
uniform float uProgress ;
void main (){
float offset = aIndex / $ { multiplier . toFixed ( 1 )};
float t = mod ( uProgress + offset , 1.0 );
vec3 pos = mix ( aStart , aEnd , easeInOut ( t ));
gl_Position = /* ... */ vec4 ( pos , 1.0 );
}
For one-time transformations during initialization, use modifiers: modifiers : {
aPosition : ( data , index , offset , instance ) => {
return data [ offset ] * Math . random ();
},
}