Keyframe Camera (Pan & Zoom)
Pan, Zoom, and Rotate Any Image Sequence Before Your Video Model Butchers It
- images
- images
- masks
Ask a video model for "camera slowly pushes in" and you're gambling. Half the frames will push in, a few will do a weird lurch, and one will decide it's a close-up now. PT Keyframe Camera is the way to stop asking and start telling: it performs the camera move itself, pixel by pixel, and hands your pipeline a sequence that has already moved.
What it actually is
Despite the Hollywood framing in the README, this is a dead-simple image-processing node - no model, no API key, no weights, no hallucination. You feed it a batch of frames and it pans, zooms, and rotates between a start state and an end state, outputting the transformed sequence. The classic use, and the one the author's README is built around: take a single image, repeat it into frames, apply a camera move here, then feed the result into a Wan I2V workflow as a VACE control signal (depth or skeleton). Your camera move is now anchored in the control data, so the video model has to honor it instead of inventing one. It also works standalone to make motion-reference clips, or chained several deep for a handheld multi-shot feel - the author literally suggests stringing a few together to fake a shoulder-mounted camera.
How it works
Linear interpolation, nothing more. The node computes progress from start_frame and end_frame (0 before the start frame, 1.0 after, linear between), then lerps every parameter toward its "end" twin. Each frame gets bilinearly resized to its zoom level, rotated around center via grid sampling, and pasted onto a canvas of the original dimensions. Whatever the move exposes is filled according to pad_mode. It's fast and deterministic - this node is not your bottleneck, ever.
The inputs that matter
- start_frame / end_frame - the timing of the move, not a frame subset. Every frame comes back; frames before
start_framehold the start values, frames afterend_framehold the end values. If you feed it 97 frames, you get 97 back. - start_horizontal_shift / end_horizontal_shift and the two vertical twins - sign is everything: positive = right/down, negative = left/up. This is the README's own #1 FAQ; get it backwards and your pan runs the wrong way and you'll swear the node is broken.
- start_zoom / end_zoom - 1.0 means no zoom. Push-in is
start<end. - zoom_origin - where zoom pivots: center, top-left, top-right, bottom-left, bottom-right.
- pad_mode -
"color"fills exposed borders withbg_color(default0, 0, 0, a comma-separated RGB string - and the parser quietly falls back to black if you feed it garbage)."edge"extends the edge pixels instead, which is what you want when black bars would confuse a control model downstream. - start_rotation / end_rotation - ±360°. One gotcha: rotation expands the canvas to the rotated bounding box, so the corners become background. Budget that when you pick
pad_mode.
The output you shouldn't ignore
It returns both images and masks. The mask is 0 where the transformed frame covers the canvas and 1 over the background - exactly what you need for compositing the original behind the move or telling a VACE/inpainting stage where the real content lives. A lot of people wire up only the images and then wonder why their edges look wrong.
Install
This is the easiest install you'll do all month. ComfyUI Manager → search "Comfyui-PT-Keyframe-Camera" (or "Keyframe Camera"), or:
cd ComfyUI/custom_nodes
git clone https://github.com/ptmaster/Comfyui-PT-Keyframe-Camera
Then restart ComfyUI. There's no requirements.txt, no model downloads, nothing beyond torch, which ComfyUI already ships. One note: it appears under the KJNodes/animation category, but it's not part of the KJNodes pack - it just borrows the shelf space, so don't go hunting for it in KJNodes' repo. The README is Chinese-first; if you need the details, let a translator chew on it.
Common gotchas
- Wrong direction - check shift signs before anything else.
- Black edges on control outputs - switch
pad_modetoedge. - Shift ceiling - shifts cap at
MAX_RESOLUTION(8192, a constant in the source). Only matters for absurd pans. - Output length - this node never truncates your sequence. If you're generating a specific frame count, do that before this node, not after.
Inputs (14)
| Name | Type | Default | Description |
|---|---|---|---|
| images | IMAGE | — | |
| start_frame | INT | 00–9999999 | — |
| end_frame | INT | 240–9999999 | — |
| start_horizontal_shift | INT | 0-8192–8192 | Horizontal shift at start frame (positive=right, negative=left) |
| start_vertical_shift | INT | 0-8192–8192 | Vertical shift at start frame (positive=down, negative=up) |
| start_rotation | INT | 0-360–360 | Rotation angle at start frame (degrees) |
| start_zoom | FLOAT | 1.000.01–10 | — |
| end_horizontal_shift | INT | 100-8192–8192 | Horizontal shift at end frame (positive=right, negative=left) |
| end_vertical_shift | INT | 0-8192–8192 | Vertical shift at end frame (positive=down, negative=up) |
| end_rotation | INT | 0-360–360 | Rotation angle at end frame (degrees) |
| end_zoom | FLOAT | 1.000.01–10 | — |
| zoom_origin | COMBO | center | 5 options: center, top-left, top-right, bottom-left, bottom-right |
| pad_mode | COMBO | color | 2 options: color, edge |
| bg_color | STRING | 0, 0, 0 | RGB values (0-255) separated by commas |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| images | IMAGE | — |
| masks | MASK | — |