Props and reactivity

Bind props to your framework's state so effects update when it changes, and animate props with an animation library.

Setting a prop

Props work like the props on any other component in your framework:

<Shader>
  <Circle color="#ff0088" radius={0.5} />
</Shader>

Binding a prop to state

Pass state instead of a fixed value, and the effect updates when the state changes. Most props are uniforms: values the GPU reads every frame, like a CSS variable the shader can see. Changing a uniform doesn't recompile the effect.

Here three buttons rotate a gradient inside a circular mask:

import { useState } from 'react'
import { Shader, LinearGradient, Circle } from 'shaders/react'

function MyComponent() {
  const [angle, setAngle] = useState(0)

  return (
    <>
      <div>
        <button onClick={() => setAngle(a => a + 45)}>Rotate +45°</button>
        <button onClick={() => setAngle(a => a - 45)}>Rotate -45°</button>
        <button onClick={() => setAngle(0)}>Reset</button>
      </div>

      <Shader>
        <Circle id="mask" radius={0.8} visible={false} />
        <LinearGradient angle={angle} maskSource="mask" />
      </Shader>
    </>
  )
}

Click the buttons and the gradient rotates on the next frame. In plain JavaScript there's no framework state, so call shader.update() with the component's id instead.

Numbers, colors and positions can all be bound the same way. Tie them to sliders, scroll position, the mouse, or any other changing value:

{/* Position and size */}
<Circle radius={size} center={{ x: posX, y: posY }} />

{/* Colors */}
<LinearGradient colorA={startColor} colorB={endColor} />

{/* Intensity */}
<Blur intensity={blurAmount} />
<Glow intensity={glowAmount} />

{/* Angles */}
<LinearGradient angle={rotation} />

Knowing which props recompile

A few props change the structure of the effect rather than a value in it, so changing them recompiles the effect. These include blendMode, maskSource, maskType, visible, and select-style props such as colorSpace on many components. A recompile costs far more than a uniform update, so don't change these props every frame.

Animating props

Any animation library that produces a changing number can drive a prop. This example uses the animate() function from Motion, which is plain JavaScript, so the same pattern works in every framework:

import { useState } from 'react'
import { animate } from 'motion'
import { Shader, LinearGradient, Circle } from 'shaders/react'

function MyComponent() {
  const [radius, setRadius] = useState(0.6)

  async function pulse() {
    // Grow, then shrink back
    await animate(radius, 1, {
      duration: 0.5,
      ease: 'easeOut',
      onUpdate: (latest) => setRadius(latest)
    })
    await animate(1, 0.6, {
      duration: 0.5,
      ease: 'easeIn',
      onUpdate: (latest) => setRadius(latest)
    })
  }

  return (
    <>
      <button onClick={pulse}>Pulse</button>

      <Shader>
        <Circle id="mask" radius={radius} visible={false} />
        <LinearGradient maskSource="mask" />
      </Shader>
    </>
  )
}

radius is a uniform, so updating it on every animation frame doesn't recompile the effect.

Next steps

  • Dynamic props: drive props from the mouse or animate them without writing animation code.
  • Hooks and events: respond to compilation and readiness events.
  • Performance: what each layer costs and how to keep effects fast.