Skip to content

Signals and animation

Stopped
The complete example source and integration notes are available below.

Read as Markdown

Full-page experience · Open playground

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

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

  • 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.
  • 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.

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.

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 built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.