Extensions/ComfyTimeline
ComfyUI Extension

ComfyTimeline

Maya-style global timeline for ComfyUI — a shared bottom-panel bar (scrub, range, transport, keyframe dope-sheet) + curve editor + frame-accurate viewer, with a public JS & Python API any node can connect to.

By FemtoStudio·Created 3 months ago·Updated about a month ago· 0
FemtoStudio/comfyui-timeline
Nodes3
On cloudLocal install
CategoryAnimation/Timeline
Stars0
Updatedabout a month ago
Readme

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, a frame-accurate image-sequence viewer, and a native 3D animation 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. The frontend works with both classic Nodes 1.0 and Vue Nodes 2.0.

The Timeline 3D Viewer uses ComfyUI's bundled native viewport and loaders. It adds no Three.js/Python dependency; frontend 1.45.15+ is required for that node (1.45.20 is the current tested target). Older frontends show a clear unsupported message while the timeline and 2D nodes keep working.

Its toolbar is identical on Nodes 1.0 and Nodes 2.0 and uploads only to ComfyUI's input/3d area. Use Upload model for a self-contained file (prefer GLB), or Upload folder for one GLTF plus its external buffers and textures. The browser safely embeds those local companions into a packed GLTF before uploading it to a unique input/3d/timeline_uploads subfolder. GLTF and GLB contents are checked for external URLs, and OBJ/MTL sidecar pairs are excluded because they are not self-contained. No additional viewer or conversion package is installed; for folders above 48 MiB, convert to GLB first.

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.
  • Timeline 3D Viewer — load/upload GLTF, GLB, OBJ, FBX, STL, PLY, or supported splat models using ComfyUI's native renderer. Choose an embedded animation clip, orbit the camera, and scrub/play it from the global timeline. Single-file and packed-GLTF-folder uploads work in both frontend generations.

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.

Execution guards reject non-finite values and pathological payloads before allocation: up to 100,000 output frames, 10,000 curve keys, a 2 MiB curve JSON store, and 10,000 image-preview frames per execution. Image previews also have a 512 MiB CPU working-set cap; their temp files follow ComfyUI's normal temp lifecycle so cached history entries remain valid.


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