Signals and animation
Full-page experience · Open playground
Compose immutable animation definitions into one component-owned program whose committed signal values enter ordinary styles explicitly.
Try it
Section titled “Try it”Click the blue card or focus it and press Enter to replay the entrance and retarget its scale.
What to learn
Section titled “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
Section titled “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
Section titled “Related APIs”Complete source
Section titled “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
Section titled “signals-and-animation.example.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
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.