jz Before/After Slider
The before/after wipe, baked into frames instead of eyeballed in a viewer
- before
- after
- images
- frame_count
- fps
Two images go in. What comes out is a whole batch of frames: a divider sweeps across the frame revealing the second image over the first, holds at the far end, sweeps back - and because the sequence starts and ends in the same state, the clip loops with no visible seam.
Why you'd reach for this one
Because the comparison is the deliverable. You upscaled a 512px image 4×, ran a LoRA at 0.4 versus 0.8, inpainted a face - the only way anyone sees the difference is side by side.
rgthree's Image Comparer is the tool everyone already has for that (97 corpus threads name it). But it's a viewer: it slides between two images inside your own session and produces no file. This node bakes the wipe into frames, so you walk away with a GIF or WebP you can paste into Discord or a model page.
How the wipe actually works
It's a render loop, not a video encoder. Each frame starts as a clone of before, and the leading x columns get overwritten with the matching columns of after - plain tensor slicing. Set orientation to horizontal and it runs the same algorithm on a transposed frame, so after comes in from the top instead.
Timing is the clever bit: five segments, half the hold at position 0, sweep out, full hold at 1, sweep back, half the hold at 0 again. With the defaults (sweep_seconds 1.5, hold_seconds 0.7, fps 20) that's 88 frames over 4.4 seconds. The halved hold is what makes it loop - 0.7s of dwell lands right on the seam, 7 frames at the tail plus 7 at the head, matching the dwell at the far end. Hold only at the end and the loop snaps.
The divider and chevron grip are drawn once with PIL and alpha-composited per frame, so they scale with scale and get clipped at the frame edges when the divider sits at 0 or 1. Turn handle off for a bare wipe.
The inputs that matter
before and after are both IMAGE and must match dimensions exactly. scale resizes both before animating and is the main lever on output size - and it's applied after the size comparison, so it can't rescue a mismatched pair. 1.0 skips resampling entirely, which is what you want if the pair is already the size you need.
sweep_seconds and hold_seconds are your pacing; fps sets frame count and playback rate. easing gives you smootherstep (zero first and second derivative at both ends, so no lurch at the turnaround) or linear. The optional interpolation only matters when scale isn't 1.0.
Outputs are images - the batch you feed to an encoder - plus frame_count (INT) and fps (FLOAT). Wire the batch into core SaveAnimatedWEBP or SaveAnimatedPNG, or into VHS_VideoCombine from VideoHelperSuite for a GIF. The frames are still just images, so you can composite a caption on them first.
Installing it
No models, no checkpoints. The pack depends on pillow, numpy and requests - all already in a working ComfyUI - and this node needs nothing beyond them. The pack's heavier bits, like the numba fast path in jz Seam Carve, are opt-in and belong to other nodes.
Via Manager, search comfyui-jz - the pack ships Comfy Registry metadata (publisher id amaunra). Or clone it:
cd ComfyUI/custom_nodes
git clone https://github.com/j-zhang19/comfyui-jz
Then restart ComfyUI; the nodes appear under jz/. It's a small personal pack from one author - custom nodes run arbitrary Python with no sandbox or review, and here that's a handful of short files you can actually skim. Only the API nodes (jz Gemini Generate, jz OpenRouter VLM) need keys, and those come from a server-side .env or config.ini; the slider touches none of it.
Where people get burned
Mismatched sizes raise. The error names both resolutions and points you at jz Resize And Pad; for a mixed set use jz Resize Long Edge (list).
A batch on either input silently animates its first frame only. Four pairs of images do not become four animations - split the batch upstream.
Both durations at zero raises - "nothing to animate". One of them has to be non-zero.
Big outputs. Past roughly 512 MB it prints a note to the console with the frame count and resolution: 88 frames at 4× scale is a multi-gigabyte batch sitting in RAM for the rest of the run. Drop scale, fps or the durations.
lanczos is the default interpolation, and comfy's implementation round-trips through 8-bit. Only relevant when scale isn't 1.0, but if you care, pick area or bicubic and stay in float.
One pack-level quirk: __init__.py imports every node module it finds, so if any other jz node fails to import, this one disappears from the menu too. Node missing after install? Read the console traceback instead of reinstalling.
For posting, GIF animates inline almost everywhere, which is usually the whole point. Keep the source PNGs - animated formats are derivative by definition.
Inputs (10)
| Name | Type | Default | Description |
|---|---|---|---|
| before | IMAGE | — | |
| after | IMAGE | revealed by the wipe, on the leading side (left, or top when horizontal) | |
| scale | FLOAT | 1.000.05–4 | resize both images before animating — the main lever on output size, and the handle scales with it. 1.0 skips resampling entirely |
| sweep_seconds | FLOAT | 1.500–60 | time for one crossing |
| hold_seconds | FLOAT | 0.700–60 | dwell at each end; the start-end hold is split across the loop seam |
| fps | INT | 201–120 | — |
| orientation | COMBO | vertical | 2 options: vertical, horizontal |
| easing | COMBO | smootherstep | 2 options: smootherstep, linear |
| handle | BOOLEAN | true | draw the divider line and chevron grip |
| interpolationopt | COMBO | lanczos | resample method for scale != 1.0 |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| images | IMAGE | — |
| frame_count | INT | — |
| fps | FLOAT | — |