Timeline Animation
The curve editor that animates your strengths — not your video
- bg_image
- float
- float_list
- count
- int
- int_list
The 30-second pitch
Timeline Animation does not make video. It makes numbers - a per-frame list of floats across your animation range, driven by a curve you draw. And that turns out to be the whole game: "animate the IPAdapter weight, the ControlNet strength, the denoise, the camera pan" without hand-typing sixty values.
Think Maya Graph Editor bolted onto a ComfyUI node. Keyframes, Hermite tangents, pan/zoom, marquee selection, key and tangent editing. Your keys appear as ticks on the pack's global timeline bar, so you can see at a glance where every keyed node sits.
Why you'd reach for it
ComfyUI animation is mostly modulation - keeping a subject locked while you slowly ramp one thing up or down. That's exactly what this node is for. It's the clean descendant of the old AnimateDiff trick where audio weights or keyframes drove ControlNet strength and motion amounts; today it slots into Wan/LTX-era workflows the same way. Whatever strength-like input you're ramping, this is the node that ramps it smoothly instead of in jarring steps.
One design choice worth calling out: keys are anchored in absolute (frame, value) coordinates. That means zooming, panning, or editing your animation range doesn't scramble what you've already drawn - a genuinely annoying failure mode in curve editors that store relative offsets.
How it works
The curve lives as JSON in a hidden keys_store widget that the JS editor owns. On execute, Python parses the keys, samples the curve at every integer frame between start and end (stepped, linear, or Hermite interpolation depending on your tangents), and clamps the results to your min/max. The playhead is deliberately left out of the cache key, so scrubbing never re-runs your graph - same design philosophy as the pack's viewer.
Inputs and outputs
The ones that matter:
min_value/max_value- defaults 0 and 1. These clamp the per-frame output, and the default is the perfect range for a strength slot, so most of the time you leave them alone.start_frame/end_frame- your animation range; driven by the global bar's anim range, hidden on the node.current_frame- display-only playhead; it does not alter output.baked_keys(optional) - feed in an existing per-frame list to seed the curve, then hit "Build from Input" in the editor to turn it into keys.keys_store- hidden, you don't touch it.bg_imageis reserved for a future viewport overlay and currently passes through unused; ignore it.
Now the outputs, because the distinction is where people trip:
float- list-expanded, so the downstream node runs once per frame with that frame's value. This is the one for per-frame strength sockets.float_list- the same data passed as a single list, for nodes that want the whole schedule at once (FizzNodes-style schedules, IPAdapter weight lists).count- how many frames you got. Handy for sanity checks.
Install
Via ComfyUI Manager (search "ComfyTimeline") or:
cd ComfyUI/custom_nodes
git clone https://github.com/FemtoStudio/comfyui-timeline
Restart, then hit Alt+T to open the Timeline panel - the bottom panel is hidden by default. No Python dependencies, no model downloads. The pack needs ComfyUI's newer node API (requires-comfyui >= 0.10.0); if the nodes won't load, update ComfyUI first.
Gotchas
- If your values never leave 0–1, that's the default clamp doing its job. Raise
max_valuewhen you need headroom. - It's a timing node, not a renderer - you still need the video/model side of the workflow. This node just drives it.
- Don't mix up the two float outputs. Wire
floatto a per-frame strength slot andfloat_listto a list-taking input; swap them and you'll get confusing per-frame vs. single-shot behavior.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| keys_store | STRING | JSON-serialized {keys, view} blob owned by the JS editor — hidden in the UI. | |
| start_frame | INT | 0-9007199254740991–9007199254740991 | First frame of the playable range. Driven by the global Timeline bar (anim-range start); hidden on the node. |
| end_frame | INT | 100-9007199254740991–9007199254740991 | Last frame of the playable range (inclusive). Driven by the global Timeline bar (anim-range end); hidden on the node. |
| min_value | FLOAT | 0.000-10000–10000 | Lower clamp on the per-frame output values. |
| max_value | FLOAT | 1.000-10000–10000 | Upper clamp on the per-frame output values. |
| bg_imageopt | IMAGE | Reference image displayed behind the curve (reserved for a future viewport overlay; currently passes through unused). | |
| baked_keysopt | FLOAT | Optional per-frame value list to seed the curve from. Index 0 maps to start_frame; values are echoed to the JS layer so the 'Build from Input' button can rebuild keys. |
Outputs (5)
| Name | Type | Description |
|---|---|---|
| float | FLOAT | — |
| float_list | FLOAT | — |
| count | INT | — |
| int | INT | — |
| int_list | INT | — |