Skip to main content

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.

This example demonstrates how to create smooth transitions between particle positions using uniforms, easing functions, and dynamic attributes.

View live demo

See transitions in action on CodePen

Complete example

Here’s a complete example that transitions particles between two positions:
import Phenomenon from 'phenomenon';

const phenomenon = new Phenomenon({
  settings: {
    devicePixelRatio: 1,
    position: { x: 0, y: 0, z: 3 },
  },
});

const multiplier = 4000;
const duration = 0.6;
const step = 0.01;

// Random position helper
function getRandom(max) {
  return Math.random() * max * 2 - max;
}

// Base positions
const start = {
  x: getRandom(1),
  y: getRandom(1),
  z: getRandom(1),
};

const end = {
  x: getRandom(1),
  y: getRandom(1),
  z: getRandom(1),
};

// Define attributes
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,
  },
  {
    name: 'aColor',
    data: () => [0.325, 0.427, 0.996],
    size: 3,
  },
  {
    name: 'aOffset',
    data: i => [i * ((1 - duration) / (multiplier - 1))],
    size: 1,
  },
];

// Progress uniform
const uniforms = {
  uProgress: {
    type: 'float',
    value: 0.0,
  },
};

// Vertex shader with easing
const vertex = `
  attribute vec3 aPositionStart;
  attribute vec3 aPositionEnd;
  attribute vec3 aPosition;
  attribute vec3 aColor;
  attribute float aOffset;

  uniform float uProgress;
  uniform mat4 uProjectionMatrix;
  uniform mat4 uModelMatrix;
  uniform mat4 uViewMatrix;

  varying vec3 vColor;

  float easeInOutQuint(float t){
    return t < 0.5 ? 16.0 * t * t * t * t * t : 1.0 + 16.0 * (--t) * t * t * t * t;
  }

  void main(){
    float tProgress = easeInOutQuint(min(1.0, max(0.0, (uProgress - aOffset)) / ${duration}));
    vec3 newPosition = mix(aPositionStart, aPositionEnd, tProgress);
    gl_Position = uProjectionMatrix * uModelMatrix * uViewMatrix * vec4(newPosition + aPosition, 1.0);
    gl_PointSize = 1.0;
    vColor = aColor;
  }
`;

const fragment = `
  precision mediump float;

  varying vec3 vColor;

  void main(){
    gl_FragColor = vec4(vColor, 1.0);
  }
`;

let forward = true;

// Add instance with animation
phenomenon.add('particles', {
  attributes,
  multiplier,
  vertex,
  fragment,
  uniforms,
  onRender: r => {
    const { uProgress } = r.uniforms;
    uProgress.value += forward ? step : -step;

    if (uProgress.value >= 1) {
      forward = false;
    } else if (uProgress.value <= 0) {
      forward = true;
    }
  },
});

Key concepts

Start and end positions

Define two sets of positions for each particle:
const attributes = [
  {
    name: 'aPositionStart',
    data: () => [x1, y1, z1],
    size: 3,
  },
  {
    name: 'aPositionEnd',
    data: () => [x2, y2, z2],
    size: 3,
  },
];

Progress uniform

Use a uniform to control the transition progress:
const uniforms = {
  uProgress: {
    type: 'float',
    value: 0.0,  // 0 = start, 1 = end
  },
};
Uniforms can be updated every frame, making them perfect for animation values that affect all particles.

Easing function

Add smooth easing in the vertex shader:
float easeInOutQuint(float t){
  return t < 0.5 ? 16.0 * t * t * t * t * t : 1.0 + 16.0 * (--t) * t * t * t * t;
}

void main(){
  float tProgress = easeInOutQuint(min(1.0, max(0.0, (uProgress - aOffset)) / 0.6));
  vec3 newPosition = mix(aPositionStart, aPositionEnd, tProgress);
  // ...
}
The mix() function interpolates between start and end positions based on the eased progress value.

Staggered animation

The aOffset attribute creates a staggered effect:
{
  name: 'aOffset',
  data: i => [i * ((1 - duration) / (multiplier - 1))],
  size: 1,
}
Each particle starts its transition at a slightly different time, creating a wave effect.

Animation loop

Update the progress uniform every frame:
onRender: r => {
  const { uProgress } = r.uniforms;
  uProgress.value += forward ? step : -step;

  if (uProgress.value >= 1) {
    forward = false;
  } else if (uProgress.value <= 0) {
    forward = true;
  }
}

Dynamic transitions

You can create continuous transitions by dynamically updating attributes when a transition completes:
onRender: r => {
  const { uProgress } = r.uniforms;
  uProgress.value += step;

  if (uProgress.value >= 1) {
    // Set start position to current end position
    r.prepareBuffer({
      name: 'aPositionStart',
      data: r.attributes[1].data,
      size: 3,
    });
    
    // Generate new end position
    const newEnd = {
      x: getRandom(1),
      y: getRandom(1),
      z: getRandom(1),
    };
    
    r.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() reuses existing data, while prepareAttribute() recalculates data using the provided function. The calculation happens on the CPU, so be mindful of performance with large particle counts.

Next steps

Easing demo

Explore different easing functions

Shapes

Combine transitions with 3D shapes