TouchDesigner Intermediate Article 10

Timeline & Animation in TouchDesigner

Create keyframe animations with the Animation COMP, sequence scenes with the Segment COMP, sync to external timecode, and build crossfading scene sequencers.

⏱ 23 min animation timeline sequencing timecode

Real-time production often requires more than live-driven visuals — you need pre-programmed cues, keyframed movements, and synchronised playback. TouchDesigner provides several complementary systems: the Animation COMP for keyframe editing, the Segment COMP for sequential cue triggering, the Time COMP for local time control, and Timecode CHOP for LTC/MTC sync with external sources.

The Animation COMP

The Animation COMP is TouchDesigner’s built-in keyframe editor. Add one from the Op Palette. Double-click it to open the animation editor — a timeline with channel lanes, a curve editor, and a scrubber.

Creating Channels and Keyframes

In the animation editor, click the + button (or right-click the channel list) to add a new channel. Name it to match the parameter you intend to animate, e.g. noise_amplitude, camera_tz, colour_hue.

Add keyframes by:

  • Right-clicking on the timeline at a specific frame and selecting “Add Key”
  • Moving the playhead to a frame and pressing K

Each key has a value and an interpolation handle. Drag the bezier handles to control easing. Available interpolation types per-key: Constant, Linear, Ease In, Ease Out, Ease In-Out, Cubic, and Custom.

The Animation COMP produces CHOP output — one channel per animation lane, one sample per frame of the current timeline.

Exporting Animation Channels to Parameters

Wire the Animation COMP’s output into a Null CHOP. Then, right-click any operator parameter in your network → Export → select the null CHOP and the matching channel name. The parameter is now driven by the keyframe animation.

For a camera dolly move:

animation1 (Animation COMP)
  └─→ null_anim (Null CHOP)
        ├─→ Export: camera1.par.tz  ← channel 'camera_tz'
        └─→ Export: camera1.par.ty  ← channel 'camera_ty'

Alternatively, use a Python expression in the parameter field directly:

# Camera COMP tz parameter expression
op('null_anim')['camera_tz'][0]

Playback Control

The Animation COMP’s playback is controlled by the Time COMP it belongs to. By default it follows the global timeline. To play it independently:

  1. Set the Animation COMP’s Timeline parameter to a specific Time COMP path.
  2. Control play/pause/loop from that Time COMP’s parameters or Python:
# Python — play and stop a local Time COMP
time = op('time1')
time.par.play = 1      # play
time.par.play = 0      # pause
time.par.frame = 0     # seek to start

Segment COMP

The Segment COMP plays back content in named sequential segments, each triggered on cue. It is the primary tool for theatrical cueing: each cue is a row in a DAT, and pressing Go (or a trigger) advances to the next cue.

Add a Segment COMP from the palette. In its Cue List (a Table DAT), each row defines one cue:

Name Duration TransitionIn TransitionOut
intro 60 fade fade
scene_A 120 cut wipe
scene_B 90 dissolve dissolve
outro 45 fade fade

Each segment can play a video, render from a Container COMP, or drive any content you assign. The Segment COMP provides channels indicating the current active segment, time within segment, and transition progress.

To advance to the next cue from Python:

op('segment1').par.gonext.pulse()

# Or jump to a specific cue by name
op('segment1').par.cuename = 'scene_B'
op('segment1').par.go.pulse()

Time COMP: Local Time Control

Every Container and Base COMP in TouchDesigner can have its own Time COMP attached, giving it an independent local timeline. Parameters on the Time COMP:

  • Rate — playback speed multiplier (1.0 = real-time, 0.5 = half speed, -1.0 = reverse)
  • Start/End Frame — loop region
  • Loop — whether playback loops
  • Play — run/pause toggle

This lets individual scenes run at their own tempo regardless of the global frame rate or other scenes:

# Gradually slow down a scene's local time
def onFrameStart(frame):
    t = op('scene_a/local_time')
    current_rate = t.par.rate.val
    t.par.rate = max(0.0, current_rate - 0.01)
    return

Animating Between States: Blend CHOP

The Blend CHOP interpolates between multiple CHOP inputs based on a weight. Use it for smooth state transitions:

state_idle  (Null CHOP — baseline values)
state_peak  (Null CHOP — peak-energy values)
  └─→ blend1 (Blend CHOP, Weight driven by audio level)
        └─→ exports to visual parameters

The Blend CHOP’s Weight parameter (0.0 → first input, 1.0 → last input) can be animated, exported, or driven by any CHOP channel. Combine with a Lag CHOP (Lag value 0.3–0.8 seconds) after the driving signal to get smooth, physics-like interpolation.

# Trigger a blend transition via Python
def trigger_transition(target_state_index, duration_seconds):
    blend = op('blend1')
    current = blend.par.weight.val
    target  = float(target_state_index)
    
    # Animate from current to target over duration
    blend.par.weight.expr = ''  # clear any expression
    blend.par.weight = current
    
    # Use a CHOP animation to drive it (cleaner approach)
    op('transition_const').par.value0 = current
    op('transition_target').par.value0 = target
    op('transition_lag').par.lag = duration_seconds

External Timecode: LTC via Audio Device

Linear Timecode (LTC) arrives as an audio signal — typically on a spare audio input from a timecode generator or DAW. The workflow in TD:

  1. Audio Device In CHOP — receives the LTC audio on a specific input channel.
  2. Timecode CHOP — decodes LTC from the audio channel into hours, minutes, seconds, frames channels.
  3. Use the decoded frame counter to control playback.
audio_in (Audio Device In CHOP, Input 1 = timecode source)
  └─→ timecode1 (Timecode CHOP)
        ├─→ hours channel
        ├─→ minutes channel
        ├─→ seconds channel
        └─→ frames channel

Convert to an absolute frame count for seeking:

# Execute DAT — sync TD playback to incoming LTC
def onFrameStart(frame):
    tc = op('timecode1')
    
    h = tc['hours'][0]
    m = tc['minutes'][0]
    s = tc['seconds'][0]
    f = tc['frames'][0]
    
    # Assume 30fps project
    abs_frame = int((h * 3600 + m * 60 + s) * 30 + f)
    
    # Seek the animation comp to match
    op('animation1').par.frame = abs_frame
    return

MIDI Timecode (MTC) Sync

MIDI Timecode arrives via the standard MIDI In CHOP. Select your MIDI device; MTC data is automatically decoded into the channel mtc_frame (absolute frame count in the session’s frame rate).

# Execute DAT — sync to MTC
def onFrameStart(frame):
    mtc_frame = op('midiIn1')['mtc_frame'][0]
    
    if mtc_frame > 0:
        op('animation1').par.frame = int(mtc_frame)
    return

Full Example: 4-Scene Sequencer with Crossfades

Goal: 4 scenes (Container COMPs), advanced by keyboard, crossfaded with a 4-second Blend TOP transition.

Network layout:

scene_intro   (Container COMP → out_top)  ──┐
scene_main    (Container COMP → out_top)  ──┼─→ blend_top (Blend TOP, Mix driven by transition_lag)
scene_climax  (Container COMP → out_top)  ──┤                └─→ window1 (Window COMP)
scene_outro   (Container COMP → out_top)  ──┘

Python controller (Script DAT, triggered by keyboard Execute DAT):

SCENES = ['scene_intro', 'scene_main', 'scene_climax', 'scene_outro']
TRANSITION_FRAMES = 4 * 60  # 4 seconds at 60fps

current_scene_idx = 0

def advance_scene():
    global current_scene_idx
    
    next_idx = (current_scene_idx + 1) % len(SCENES)
    crossfade_to(next_idx)
    current_scene_idx = next_idx

def crossfade_to(idx):
    # Tell the Blend TOP which input to transition toward
    # Blend TOP input order matches SCENES list
    blend = op('blend_top')
    
    # Set input weights: smoothly move weight to idx
    for i, _ in enumerate(SCENES):
        blend.par[f'weight{i}'] = 1.0 if i == idx else 0.0
    
    # Drive the mix with a lag (4 second lag = 4-second crossfade)
    op('blend_lag').par.lag = TRANSITION_FRAMES / 60.0

def go_to_scene(name):
    global current_scene_idx
    if name in SCENES:
        idx = SCENES.index(name)
        crossfade_to(idx)
        current_scene_idx = idx

Keyboard Execute DAT:

def onKey(key, character, alt, lshift, rshift, lctrl, rctrl, lAlt, rAlt, state):
    if state == 'press':
        if character == ' ':
            run('advance_scene()', fromOP=op('sequencer_script'))
        elif character in '1234':
            idx = int(character) - 1
            run(f'go_to_scene(SCENES[{idx}])', fromOP=op('sequencer_script'))
    return

The Blend TOP with four inputs and a Lag CHOP-driven mix parameter handles the visual crossfade. Each Container COMP runs its own Time COMP independently, so the scenes can be in mid-playback at any point when they come into focus — no hard jumps.