ComfyTimeline — a global timeline standard for ComfyUI

A VFX-style global timeline for ComfyUI — one bottom-panel bar that owns frame state (playhead, animation range, playback range, fps, transport, loop) for the whole graph, plus a VFX Graph-Editor-style curve editor node and a frame-accurate image-sequence viewer.

It also ships ComfyTimeline, a small public API so any node or extension can connect to the timeline — read the current frame/range/fps, react to scrubbing and playback, or draw keyframe ticks on the bar — from both JS and Python.

Status: apiVersion 1.0.0. The surface listed in API_REFERENCE.md is the committed stable contract.


Install

cd ComfyUI/custom_nodes
git clone https://github.com/FemtoStudio/comfyui-timeline

Restart ComfyUI, then open the bottom panel → "Timeline" tab (the panel is hidden by default — toggle it from View → Bottom Panel, or press Alt+T to jump straight to the Timeline tab). No Python dependencies beyond ComfyUI itself.

Transport shortcuts

Defaults (rebindable in Settings → Keybindings, discoverable in the command palette under "Timeline:"):

key action
Space play / pause
→ / ← next / previous frame (wraps in the play range)
Alt+T open the Timeline panel tab

Transport buttons on the bar: ⏮ go-to-start · ‹ step · ▶/⏸ play · › step · ⏭ go-to-end · ⟳ loop · EF play-every-frame (no frame-drop, vs real-time).

Nodes provided:

  • Timeline Animation — author animation curves (Hermite tangents, pan/zoom, marquee, key/tangent editing). Its keys show as ticks on the global bar. Outputs: float (list-expanded, one downstream run per frame), float_list (the same samples as one list), count, and int / int_list — the samples rounded to true integers for INT-typed inputs (frame picks, seeds, step counts).
  • Timeline Image Viewer — scrub/play an IMAGE batch against the global playhead. Optional mask input mattes each frame before display (white = keep, black = out); a single mask broadcasts across the batch and resolution mismatches are resized.

Scrubbing never re-runs your graph: the playhead lives on the timeline model, not in any node input, so queueing after a scrub is a clean cache hit.


Connect a node to the timeline

Python (read the frame/range/fps in execute())

from comfy_api.latest import io
try:
    from comfy_timeline import timeline_inputs, read_timeline
except ImportError:                      # timeline pack not installed
    timeline_inputs = lambda **k: []
    read_timeline = None

class MyNode(io.ComfyNode):
    @classmethod
    def define_schema(cls):
        return io.Schema(
            node_id="MyNode",
            inputs=[io.Image.Input("image"), *timeline_inputs()],
        )

    @classmethod
    def execute(cls, image, **tl):
        t = read_timeline(**tl)
        frame = t.current_frame          # the live playhead at queue time
        ...

timeline_inputs() adds standard current_frame / start_frame / end_frame / fps widgets. The frontend auto-binds and hides them and drives them from the bar, so the value the user set on the bar bakes into the prompt at queue time — cache-correct, no server state. Don't convert these to socket inputs (force_input); they must stay widgets.

JS (react to the timeline live — no boilerplate for "read")

If your node already declares the standard widgets (via timeline_inputs() or by hand: current_frame, or both start_frame+end_frame), the auto-bind wires them with zero JS.

To react (e.g. repaint at the new frame), subscribe to the model:

import { app } from "/scripts/app.js";

app.registerExtension({
  name: "MyExt",
  setup() {
    const T = window.ComfyTimeline;          // capability check
    if (!T || parseInt(T.apiVersion) < 1) return;
    const off = T.model.subscribe(["frame", "range"], (e) => {
      myRepaint(T.model.currentFrame);       // or e.detail.frame
    });
    // call off() to unsubscribe
  },
});

To drive the timeline (e.g. a "shot info" node sets the range):

window.ComfyTimeline.model.setAnimRange(1001, 1100);
window.ComfyTimeline.model.setPlayRange(1001, 1100);

Events

window.ComfyTimeline.model is an EventTarget. Each event's detail carries a source token — pass your own stable token to mutators and ignore events where detail.source is your own, to avoid feedback loops.

event detail when
frame { frame:int, source } playhead moved
range { which:'anim'|'play', animStart, animEnd, playStart, playEnd, source } a range changed
fps { fps:float, source } fps (or speed) changed
play { playing:bool, source } transport toggled
loop { loop:bool, source } loop toggled
mode { mode:'realtime'|'everyframe', source } playback mode toggled
change (on curves) { curveId } a curve was registered / removed / edited

Full method/field reference: API_REFERENCE.md. TypeScript/editor typings: web/js/comfytimeline.d.ts.


Versioning

  • window.ComfyTimeline.apiVersion — semver of the public surface. Minor = additive, major = breaking. Feature-detect with it.
  • window.ComfyTimeline.schemaVersion — integer; only the app.graph.extra.timeline serialization. Independent of apiVersion.

Migration

The pre-1.0 window.timelineAnim cross-node registry has been removed. Use window.ComfyTimeline (model/curves/events) instead.

License

MIT — see LICENSE.

S
Description
No description provided
Readme MIT
310 KiB
Languages
JavaScript 52.7%
Python 46.5%
CSS 0.8%