Nodes/WAS_Extras/Camera Motion Trajectory from Images
ComfyUI Node

Camera Motion Trajectory from Images

Ken Burns effects with a keyframed JSON spec

By WASasquatch·Created 3 years ago·Updated 17 days ago· 43
Camera Motion Trajectory from Images
  • image
  • depth_map
  • video
  • frame_count
num_frames60
trajectory_spec{ "loop": false, "default_ease": "linear", "keyframes": [ { "frame": 0, "zoom": 1.0, "center": [0.5, 0.5], "angle": 0.0, "pan": [0.0, 0.0], "tilt": [0.0, 0.0], "dolly_strength": 0.0, "dolly_radius": [0.3, 0.3], "dolly_feather": 0.5, "dolly_mode": "radial", "sphereize_strength": 0.0, "sphereize_radius": [0.4, 0.4], "sphereize_feather": 0.5, "depth_strength": 0.0, "ease": "ease_in_out" }, { "frame": 30, "zoom": 1.5, "center": [0.5, 0.5], "angle": 90.0, "pan": [0.0, 0.0], "tilt": [0.0, 0.15], "dolly_strength": 0.3, "dolly_radius": [0.35, 0.35], "dolly_feather": 0.5, "dolly_mode": "radial", "sphereize_strength": 0.0, "sphereize_radius": [0.4, 0.4], "sphereize_feather": 0.5, "depth_strength": 0.0, "ease": "ease_in_out" }, { "frame": 59, "zoom": 2.0, "center": [0.6, 0.4], "angle": 360.0, "pan": [0.1, 0.0], "tilt": [0.0, 0.3], "dolly_strength": 0.6, "dolly_radius": [0.4, 0.4], "dolly_feather": 0.5, "dolly_mode": "radial", "sphereize_strength": 0.0, "sphereize_radius": [0.4, 0.4], "sphereize_feather": 0.5, "depth_strength": 0.0, "ease": "linear" } ] }
edge_modemirror
enable_camera_shakefalse
shake_position_amplitude0.030
shake_rotation_amplitude1.5
shake_seed0

Before I2V models got good at inventing plausible motion from a still, the standard way to turn a photo into "video" was the Ken Burns effect - pan, zoom, maybe a slow rotate, entirely deterministic, no new content generated. This node is that technique, done properly: a keyframed camera path (pan, tilt, rotation, zoom, plus dolly and a "sphereize" warp) over a still image, with easing between keyframes, producing an actual frame sequence without touching a video model at all.

How it works

The whole trajectory is defined in one place: trajectory_spec, a multiline JSON blob with a list of keyframes, each specifying a frame number and the camera state at that point - zoom, center, angle (rotation), pan, tilt, plus dolly_strength/dolly_radius/dolly_feather/dolly_mode for a push-in/out effect, and sphereize_strength/sphereize_radius/sphereize_feather for a fisheye-style warp. Each keyframe also carries its own ease curve, and there's a top-level default_ease and loop flag for the whole sequence. There's no visual curve editor here - the JSON is the interface, and the node ships with a working three-keyframe example (zoom 1.0 → 1.5 → 2.0, rotating a full 360° across frames 0/30/59) that's worth running once as-is before writing your own.

The optional depth_map input is what elevates this past a flat 2D pan-and-zoom: feed it a depth estimate of your image (from a depth estimation node) and the camera move gets real parallax - near objects shift more than far ones as the "camera" moves, instead of the whole image sliding as one flat plane. Without a depth map, you get a straightforward 2D Ken Burns move; with one, you get something closer to a fake dolly shot.

The inputs and outputs that matter

  • image (IMAGE) - the still you're animating.
  • num_frames (default 60, range 1–2048) - how many frames to render. Should generally match the frame range your trajectory_spec keyframes actually cover.
  • trajectory_spec (multiline STRING, JSON) - the entire camera path. This is the node's real interface; everything else is secondary.
  • edge_mode (border / mirror / wrap, default mirror) - how to fill pixels that get revealed at the frame edge once you pan or zoom out. Matters as soon as your trajectory pushes content past the original image bounds.
  • depth_map (optional, IMAGE) - feed this for parallax/dolly effects; without it, motion is flat.

Two outputs: video (IMAGE, a batch of rendered frames) and frame_count (INT), which is convenient to wire onward into anything downstream that needs to know how many frames you actually produced.

How to install it

Via ComfyUI Manager: search "WAS_Extras", install, restart. Manually:

cd ComfyUI/custom_nodes
git clone https://github.com/WASasquatch/WAS_Extras

Restart ComfyUI. No extra dependencies for this node.

Common issues & troubleshooting

Edges of the frame look smeared or repeated once the camera pans. That's edge_mode. mirror is usually the least jarring default for photos; border repeats edge pixels outward (can look like smearing on a wide pan); wrap tiles the image, which is rarely what you want outside of deliberately tileable source images.

The trajectory doesn't seem to do anything, or errors out. Check your trajectory_spec is valid JSON first - a malformed edit (a missing comma, an unclosed bracket) is the most likely cause if you were hand-editing the default spec rather than replacing it wholesale.

Motion looks flat and "slidey" rather than like a real camera move. That's expected without a depth_map - a flat pan/zoom moves every pixel by the same amount regardless of what's near or far in the scene. Feed it a depth map from a depth estimation node if you want actual parallax.

Camera move feels too smooth or mechanically perfect. That's what enable_camera_shake (with shake_position_amplitude, shake_rotation_amplitude, and shake_seed) is for - it layers a small amount of handheld-style jitter on top of the deterministic path, which can make an otherwise perfectly clean move read as less artificial.

CategoryWAS/Video

Inputs (9)

NameTypeDefaultDescription
imageIMAGE
num_framesINT601–2048
trajectory_specSTRING{ "loop": false, "default_ease": "linear", "keyframes": [ { "frame": 0, "zoom": 1.0, "center": [0.5, 0.5], "angle": 0.0, "pan": [0.0, 0.0], "tilt": [0.0, 0.0], "dolly_strength": 0.0, "dolly_radius": [0.3, 0.3], "dolly_feather": 0.5, "dolly_mode": "radial", "sphereize_strength": 0.0, "sphereize_radius": [0.4, 0.4], "sphereize_feather": 0.5, "depth_strength": 0.0, "ease": "ease_in_out" }, { "frame": 30, "zoom": 1.5, "center": [0.5, 0.5], "angle": 90.0, "pan": [0.0, 0.0], "tilt": [0.0, 0.15], "dolly_strength": 0.3, "dolly_radius": [0.35, 0.35], "dolly_feather": 0.5, "dolly_mode": "radial", "sphereize_strength": 0.0, "sphereize_radius": [0.4, 0.4], "sphereize_feather": 0.5, "depth_strength": 0.0, "ease": "ease_in_out" }, { "frame": 59, "zoom": 2.0, "center": [0.6, 0.4], "angle": 360.0, "pan": [0.1, 0.0], "tilt": [0.0, 0.3], "dolly_strength": 0.6, "dolly_radius": [0.4, 0.4], "dolly_feather": 0.5, "dolly_mode": "radial", "sphereize_strength": 0.0, "sphereize_radius": [0.4, 0.4], "sphereize_feather": 0.5, "depth_strength": 0.0, "ease": "linear" } ] }
edge_modeCOMBOmirror3 options: border, mirror, wrap
enable_camera_shakeBOOLEANfalse
shake_position_amplitudeFLOAT0.0300–0.5
shake_rotation_amplitudeFLOAT1.50–45
shake_seedINT00–2147483647
depth_mapoptIMAGE

Outputs (2)

NameTypeDescription
videoIMAGE
frame_countINT