# Signals and animation
[Full-page experience](/experience/signals-and-animation/) · [Open playground](/playground/#/examples/composition/signals-and-animation)

Compose immutable animation definitions into one component-owned program whose committed signal values enter ordinary styles explicitly.

## Try it

Click the blue card or focus it and press Enter to replay the entrance and retarget its scale.

## What to learn

- Bind tween and spring definitions to writable signals, then compose their tracks with parallel, sequence, and a named marker.
- Keep playback ownership in one component while every animation advances and commits through Pibbl's central frame scheduler.

## Integration boundaries

- Definitions and programs are immutable descriptions; usePlayback supplies the stateful component owner and snapshots an accepted run.
- All playbacks advance in shared scheduler phases and publish one central Commit before affected Pibbl roots render.
- Signals can flow directly into declared Pibbl inputs; the receiving component resolves them and owns the dependency.

This is a Pibbl Canvas example. Its mount owns its runtime resources; call its controller's dispose method when removing it. The preview starts automatically unless reduced motion is enabled; Stop disposes the mount. Preserve the source's logical canvas size and aspect ratio. Native HTML is not captured by Canvas filters.

## Related APIs

- [useSignal](/reference/hooks/use-signal/)
- [useAnimatedValue](/reference/hooks/use-animated-value/)
- [usePlayback](/reference/hooks/use-playback/)
- [parallel](/reference/functions/parallel/)
- [sequence](/reference/functions/sequence/)
- [marker](/reference/functions/marker/)
- [spring](/reference/functions/spring/)

## Complete source

Host setup for this source: use a canvas with width 720 and height 420, compile with `jsx: "react-jsx"` and `jsxImportSource: "@pibbl/core"`, import its default `mount`, and call `const controller = mount(canvas)` after attaching the canvas. Call `controller.dispose()` before removing it.

### signals-and-animation.example.tsx

```tsx
import {
  pibbl,
  usePlayback,
  useSignal,
  useAnimatedValue,
  FocusManagement,
  Group,
  marker,
  parallel,
  Rectangle,
  sequence,
  spring,
  Text,
  drive,
  tween,
  type PibblController,
} from "@pibbl/core";

function AnimatedCard() {
  const x = useSignal(110, { debugName: "learning animation x" });
  const opacity = useSignal(0.2, {
    debugName: "learning animation opacity",
  });
  const pulseScale = useSignal(1, {
    debugName: "learning animation pulse scale",
  });
  const targetScale = useSignal(1);
  const message = useSignal("CLICK OR PRESS ENTER");
  const presentedScale = useAnimatedValue(targetScale.get(), { animation: spring({ stiffness: 240, damping: 22 }), initial: 1, debugName: "learning target scale" });

  const entrance = sequence(
    parallel(
      drive(
        x,
        tween({
          from: 110,
          to: 178,
          duration: 360,
          debugName: "learning entrance x",
        }),
      ),
      drive(
        opacity,
        tween({
          from: 0.2,
          to: 1,
          duration: 360,
          debugName: "learning entrance opacity",
        }),
      ),
      drive(
        pulseScale,
        tween({
          from: 1,
          to: 1,
          duration: 0,
          debugName: "learning pulse reset",
        }),
      ),
    ),
    marker("arrived"),
    drive(
      pulseScale,
      spring({
        from: 1,
        to: 1.08,
        stiffness: 300,
        damping: 18,
        debugName: "learning pulse scale",
      }),
    ),
  );
  const playback = usePlayback(entrance, {
    debugName: "learning entrance playback",
    onEvent: (event) => {
      if (event.type === "marker" && event.marker === "arrived") {
        message.set("ARRIVED · ONE COMMITTED FRAME");
      }
    },
  });

  const currentX = x.get();
  const currentOpacity = opacity.get();
  const currentScale = presentedScale.get() * pulseScale.get();

  return [
    <Rectangle style={{ width: 720, height: 420, fill: "#eef2ff" }} />,
    <Text
      style={{
        left: 360,
        top: 58,
        fill: "#17211d",
        font: "900 24px sans-serif",
        textAlign: "center",
      }}
    >
      SIGNALS + ANIMATION
    </Text>,
    <Text
      style={{
        left: 360,
        top: 84,
        fill: "#475569",
        font: "600 12px monospace",
        textAlign: "center",
      }}
    >
      DEFINITIONS → PROGRAM → OWNED PLAYBACK → CENTRAL COMMIT
    </Text>,
    <Group
      style={{
        translateX: currentX,
        translateY: 126,
        scaleX: currentScale,
        scaleY: currentScale,
      }}
    >
      <Rectangle
        style={{
          width: 364,
          height: 172,
          fill: "#2563eb",
          stroke: "#17211d",
          strokeWidth: 6,
          opacity: currentOpacity,
          cursor: "pointer",
        }}
        onClick={() => {
          targetScale.update((value) => (value === 1 ? 0.96 : 1));
          message.set("PLAYING IMMUTABLE PROGRAM");
          playback.play({ conflict: "replace" });
        }}
      />
      <Text
        style={{
          left: 182,
          top: 68,
          fill: "#ffffff",
          font: "900 22px sans-serif",
          textAlign: "center",
        }}
      >
        EXPLICIT .get()
      </Text>
      <Text
        style={{
          left: 182,
          top: 108,
          fill: "#f7c948",
          font: "700 12px monospace",
          textAlign: "center",
        }}
      >
        {message.get()}
      </Text>
      <Text
        style={{
          left: 182,
          top: 138,
          fill: "#dbeafe",
          font: "600 11px monospace",
          textAlign: "center",
        }}
      >
        STYLE RECEIVES NUMBERS, NOT SIGNAL OBJECTS
      </Text>
    </Group>,
  ];
}

export default function mount(canvas: HTMLCanvasElement): PibblController {
  return pibbl(
    canvas,
    <FocusManagement>
      <AnimatedCard />
    </FocusManagement>,
  );
}

```

## Documentation version

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.
