Dynamic props

Drive any numeric or position prop from time, the cursor, or another layer, without writing animation code.

A dynamic prop is an object you pass in place of a prop's value. Its type field names what drives the value: auto-animate, mouse-position, mouse, or map. Here the same radius prop is set statically, then animated:

<!-- Static value -->
<Circle :radius="0.5" />

<!-- Dynamic prop: radius moves between 0.2 and 0.6 on its own -->
<Circle :radius="{ type: 'auto-animate', mode: 'ping-pong', outputMin: 0.2, outputMax: 0.6 }" />

Animating a prop over time

The auto-animate type moves a numeric prop between two values, forever. Set the range with outputMin and outputMax, and the pace with speed:

<Shader className="w-full h-64">
  <Circle
    color="#6366f1"
    radius={{
      type: 'auto-animate',
      mode: 'ping-pong',
      outputMin: 0.2,
      outputMax: 0.6,
      speed: 0.8,
      easing: 'sine'
    }}
  />
</Shader>

The auto-animate fields:

FieldTypeDefaultDescription
mode'ping-pong' | 'loop'requiredping-pong oscillates between min and max. loop advances from min to max, then wraps.
outputMinnumberrequiredValue at the start of the range.
outputMaxnumberrequiredValue at the end of the range.
speednumber1.0Cycles per second. Negative values reverse the loop direction.
easingstring'sine'Easing curve, ping-pong mode only: sine, linear, quad, expo, or bounce.

Use it for pulsing shapes, breathing glow intensities, or looping gradient angles. For a continuous rotation, use loop mode from outputMin: 0 to outputMax: 360.

Following the cursor

The mouse-position type drives a position prop from the cursor. Use it on any prop that takes { x, y } coordinates, such as center, position, or offset:

<Shader className="w-full h-64">
  <Circle
    color="#6366f1"
    radius={0.15}
    center={{
      type: 'mouse-position',
      smoothing: 0.12,
      momentum: 0.2
    }}
  />
</Shader>

smoothing makes the circle lag behind the cursor, and momentum lets it overshoot and spring back. The mouse-position fields:

FieldTypeDefaultDescription
x'mouse' | number'mouse''mouse' tracks cursor X. A number pins X to that value (0–1).
y'mouse' | number'mouse''mouse' tracks cursor Y. A number pins Y to that value (0–1).
invertXbooleanfalseFlips the X direction: moving the cursor right moves the position left.
invertYbooleanfalseFlips the Y direction.
smoothingnumber0Lag (0–1). 0 follows instantly; higher values follow more slowly.
momentumnumber0Spring bounce (0–1). 0 has no overshoot; values near 1 are springy.
reachnumber1Scales how far the position travels from the origin point. 1 tracks the cursor 1:1, 2 travels twice as far, 0 stays at the origin point.
originXnumber0.5X coordinate of the origin point that reach scales from.
originYnumber0.5Y coordinate of the origin point.

To track one axis only, set the other to a number. This circle follows the cursor horizontally and stays at the vertical center:

<Circle :radius="0.15" :center="{ type: 'mouse-position', y: 0.5 }" />

Mapping the cursor to a number

The mouse type drives a single numeric prop from the cursor's X or Y position. Here the circle grows as the cursor moves right:

<Shader className="w-full h-64">
  <Circle
    color="#6366f1"
    radius={{
      type: 'mouse',
      axis: 'x',
      outputMin: 0.1,
      outputMax: 0.7,
      smoothing: 0.1
    }}
  />
</Shader>

The mouse fields:

FieldTypeDefaultDescription
axis'x' | 'y'requiredWhich cursor axis drives the prop.
outputMinnumberrequiredValue when the axis is at 0 (left or top).
outputMaxnumberrequiredValue when the axis is at 1 (right or bottom).
curvenumber0Power curve, -1 to +1. Negative values bias toward outputMin, positive toward outputMax.
smoothingnumber0Lag (0–1), as in mouse-position.
momentumnumber0Spring bounce (0–1), as in mouse-position.

Use it to set a blur amount from the horizontal position, brightness from the vertical position, a hue shift, or a zoom intensity.

Driving a prop from another layer

The map type drives a numeric prop from what another layer draws. It reads one channel of that layer, either its alpha (opacity) or its luminance (brightness), and turns it into a value. Picture CSS mask-image, except the image sets a number instead of visibility.

Each pixel reads the source at its own position, so the driven prop can vary across the canvas. (DotGrid and Grid read it once per cell, so each dot gets one size.) Give the source component an id, then name that id as the source:

<Shader className="w-full h-64">
  <LinearGradient
    id="grad"
    colorA="#000000"
    colorB="#ffffff"
  />
  <DotGrid
    dotSize={{
      type: 'map',
      source: 'grad',
      channel: 'luminance',
      inputMin: 0,
      inputMax: 1,
      outputMin: 0.02,
      outputMax: 0.12
    }}
  />
</Shader>

The demo hides the gradient with visible: false, and the dots still read it. The map fields:

FieldTypeDefaultDescription
sourcestringrequiredThe id of the component to read from.
channelstringrequiredWhich channel to read: alpha, alphaInverted, luminance, or luminanceInverted.
inputMinnumberrequiredSource values at or below this count as 0.
inputMaxnumberrequiredSource values at or above this count as 1.
outputMinnumberrequiredValue when the input is 0.
outputMaxnumberrequiredValue when the input is 1.
curvenumber0Power curve applied after the input is normalized, -1 to +1.

The first map driver renders its source layer to a texture, an offscreen image the driven prop samples from. The source layer renders into that texture every frame, so each map source adds one render pass.

Combining dynamic props

A component can have several dynamic props at once, one per prop. This lens flare follows the cursor while its intensity pulses:

<Shader className="w-full h-64">
  <LinearGradient colorA="#0f172a" colorB="#4f46e5" />
  <LensFlare
    center={{ type: 'mouse-position', smoothing: 0.1 }}
    intensity={{
      type: 'auto-animate',
      mode: 'ping-pong',
      outputMin: 0.4,
      outputMax: 1.0,
      speed: 0.6
    }}
  />
</Shader>

Switching at runtime with the JS API

In the JavaScript API, shader.update() accepts dynamic props too, so a prop can switch between a static value and a dynamic one at any point:

// Start with a static value
const shader = await createShader(canvas, {
  components: [
    { type: 'Circle', id: 'c', props: { radius: 0.4 } }
  ]
})

// Later: switch to animated
shader.update('c', {
  radius: {
    type: 'auto-animate',
    mode: 'ping-pong',
    outputMin: 0.2,
    outputMax: 0.7
  }
})

// Later: switch back to static
shader.update('c', { radius: 0.4 })

Next steps