Interpolation

interpolate and interpolateColor map a shared value through a piecewise-linear curve and return a binding for an { animated } style position. One value can then drive several properties, each over its own range, and the mapping is evaluated on the Bevy side every frame.

Usage

import {
  interpolate,
  interpolateColor,
  useSharedValue,
  withTiming,
} from "bevy-react";

function Card() {
  const t = useSharedValue(0);
  return (
    <node
      onPointerEnter={() => {
        t.value = withTiming(1, { duration: 200 });
      }}
      onPointerLeave={() => {
        t.value = withTiming(0, { duration: 200 });
      }}
      style={{
        width: 120,
        height: 80,
        transform: {
          scale: { animated: interpolate(t, [0, 1], [1, 1.1]) },
        },
        backgroundColor: {
          animated: interpolateColor(t, [0, 1], ["#7aa2f7", "#f7768e"]),
        },
      }}
    />
  );
}

The shared value runs from 0 to 1, the scale from 1 to 1.1 and the color from blue to red. Setting the value directly (t.value = 0.5, from a slider for example) moves every binding at once, with no driver at all.

interpolate

interpolate(value, input, output) returns a number binding:

  • input lists readings of the shared value in ascending order, and output the result at each of them, in the units of the position it binds: pixels for lengths, degrees for rotations (see Animated values).

  • Between two neighbouring stops the result is linear. Use as many stops as you need:

    translateY: {
      animated: interpolate(
        t,
        [0, 0.25, 0.5, 0.75, 1],
        [34, -34, 34, -34, 34],
      ),
    },
  • Outside the input range the result clamps to the first or last output. There is no extrapolation option; add stops beyond the range where you want the curve to continue.

  • A repeated input stop makes a step: input: [0, 0.5, 0.5, 1] with output: [0, 0, 1, 1] jumps from 0 to 1 at 0.5.

  • Extra stops in the longer of the two lists are ignored. A single stop gives a constant.

Clamping makes staggering easy: give each item its own window of the same value, and it sits at an end of its range outside that window.

items.map((item, i) => (
  <node
    key={item.id}
    style={{
      width: 18,
      height: {
        animated: interpolate(t, [i * 0.16, i * 0.16 + 0.36], [10, 68]),
      },
    }}
  />
));

interpolateColor

interpolateColor(value, input, colors) returns a color binding, for the positions that take one: backgroundColor, borderColor, color, a backgroundImage tint, gradient stop colors and color filter params.

  • colors are hex strings: #rgb, #rrggbb, or #rrggbbaa with alpha. Other color syntaxes (rgb(), named colors, #rgba) are not understood; the binding is rejected with a styleBinding warning and does nothing.
  • Colors are interpolated channel by channel in sRGB, alpha included.
  • Stops, ordering and clamping work as for interpolate.

With drivers

The bindings follow the shared value whatever moves it, so animate one progress value and map it per property. Because the curve clamps, a spring that overshoots past the last stop holds at the last output; extend the stops when the overshoot should show:

t.value = withSpring(1, { damping: 8 });

scale: { animated: interpolate(t, [0, 1, 2], [1, 1.1, 1.2]) },

Writing the binding inline in render costs nothing extra: an unchanged binding compares equal and sends no update.

Limits

  • Curves are piecewise linear only. Shape the timing with the driver's easing or spring instead.
  • interpolateColor takes hex colors only and interpolates in sRGB, which can pass through muddy midpoints between distant hues. Add an intermediate stop to steer it.
  • Bindings, interpolated or not, work in the base style only.

The Animatable column of the style reference marks every property that accepts a binding.