@isirmt/react-cues
v0.2.1
Published
Declarative timeline and temporal state engine for React
Readme
@isirmt/react-cues
A declarative timeline and temporal state engine for React.
Experimental: This package is in early development. APIs may change before
1.0.0.
Features
- Define state changes and effects on a timeline
- Play, pause, seek, and restart after finishing
- Run effects only during forward playback
- Subscribe to snapshots from React
- Use the core timeline independently from React
- Build seekable continuous animations through the Web Animations API
- Change playback speed by wrapping any clock
- Loop timelines and toggle looping during playback
Installation
pnpm add @isirmt/react-cues(React 18 or 19 is required)
Quick Start
import {
at,
defineTimeline,
effect,
set,
useTimeline,
} from "@isirmt/react-cues";
type DemoState = {
phase: "idle" | "active" | "done";
visible: boolean;
};
const definition = defineTimeline<DemoState>({
initialState: {
phase: "idle",
visible: false,
},
duration: 1500,
cues: [
at(0, set("visible", true)),
at(500, set("phase", "active")),
at(
1000,
effect(() => {
console.log("1000ms");
}),
),
at(1500, set("phase", "done")),
],
});
export function Demo() {
const { timeline, snapshot } = useTimeline(definition);
const { currentTime, duration, status, state } = snapshot;
return (
<section className={state.visible ? "field visible" : "field"}>
<p>Time: {currentTime}ms</p>
<p>Status: {status}</p>
<p>Phase: {state.phase}</p>
<button onClick={() => timeline.play()}>Play</button>
<button onClick={() => timeline.pause()}>Pause</button>
<button onClick={() => timeline.seek(0)}>Reset</button>
<input
type="range"
min={0}
max={duration}
value={currentTime}
onChange={(event) => {
timeline.seek(Number(event.currentTarget.value));
}}
/>
</section>
);
}WAAPI
Use animate() to place continuous animation clips on the timeline.
import {
animate,
at,
defineTimeline,
useWaapiTimeline,
} from "@isirmt/react-cues";
const changeBackground = animate(
"background",
[{ backgroundColor: "#0f172a" }, { backgroundColor: "#2563eb" }],
{ duration: 4000, easing: "linear" },
);
const showSparkle = animate(
"sparkle",
[
{ opacity: 0, transform: "translateY(20px)" },
{ opacity: 1, transform: "translateY(0)" },
],
{ duration: 500, easing: "ease-out" },
);
const moveSparkle = animate(
"sparkle",
[
{ transform: "translateY(0) scale(1)" },
{ transform: "translateY(-40px) scale(1.4)" },
],
{ duration: 1000, easing: "ease-in-out" },
);
const definition = defineTimeline({
initialState: {},
cues: [at(0, changeBackground), at(1500, showSparkle), at(2500, moveSparkle)],
});
export function WaapiDemo() {
const { timeline, snapshot, bind } = useWaapiTimeline(definition);
return (
<section
ref={bind("background")}
style={{
paddingTop: 64,
color: "white",
backgroundColor: "#0f172a",
}}
>
<span
ref={bind("sparkle")}
style={{
display: "inline-block",
fontSize: 64,
opacity: 0,
}}
>
✦
</span>
<div>
<button onClick={() => timeline.play()}>Play</button>
<button onClick={() => timeline.pause()}>Pause</button>
</div>
<input
type="range"
min={0}
max={snapshot.duration}
value={snapshot.currentTime}
onChange={(event) => timeline.seek(Number(event.currentTarget.value))}
/>
</section>
);
}Playback speed and looping
import {
AnimationFrameClock,
PlaybackRateClock,
createTimeline,
} from "@isirmt/react-cues";
const clock = new PlaybackRateClock(new AnimationFrameClock(), 2);
const timeline = createTimeline(definition, {
clock,
loop: true,
});
clock.setPlaybackRate(0.5);
timeline.setLoop(false);Development
pnpm install
pnpm run test
pnpm run typecheck
pnpm run build
pnpm run format:checkLicense
MIT
