Field Gradient
Six Ramps and a Curve Editor That Doesn't Suck
- reference_image
- reference_mask
- mask
- preview
The gradient node you'd expect to be a one-liner
In most packs a "gradient" node is a linear black-to-white ramp with an angle widget, and you outgrow it in an afternoon. Field Gradient ships six ramp geometries and a stops-ramp profile editor, which sounds like overkill until you want a radial vignette with a soft shoulder, or an angular sweep that isn't linear, or a repeating ramp that hard-wraps instead of tiling with a visible seam.
The six modes: linear_u (left to right), linear_v (top to bottom), radial, diamond, box and angular. Position is center_x / center_y in window fractions (0.5 is the frame centre, and the range runs −1 to 2 so you can put the origin off-frame), and rotation in degrees, counter-clockwise. Radial ignores rotation, which is correct - a Euclidean distance is rotation-invariant.
The profile is where the interesting part lives
The ramp widget is a stops ramp: a horizontal strip that draws the evaluated curve with draggable stop handles underneath. Same control you know from Houdini or Substance. Double-click the strip to add a stop, drag a stop out of the strip to remove it, and each stop carries an interpolation for the segment to its right - constant, linear or smooth. Two stops at the same position make a hard jump, and the later one wins from there onward.
smooth is the quintic 6t⁵ − 15t⁴ + 10t³, not the cubic smoothstep. That's a deliberate choice across the whole pack: the cubic's second-derivative jump shows up downstream in anything slope-sensitive, and this thing exists to feed other nodes.
Crucially, the JSON under the canvas is the node input. That means an API workflow can write exactly what the widget writes, and you can paste one in by hand:
{"version": 1, "stops": [{"p": 0.0, "v": 0.0, "i": "linear"},
{"p": 1.0, "v": 1.0, "i": "linear"}]}
Validation is loud rather than clever: malformed JSON, NaN, an unknown interpolation name, or a stop count outside 1..64 raises an error naming the offending index. No silently rendering something plausible from a bad string, which is more than most JSON-in-a-widget nodes manage.
The rest of the widgets
repeat tiles the ramp across the declared range and phase slides it before wrapping. mirror triangle-folds instead of hard-wrapping, which is the difference between a repeating stripe with a seam and a smooth alternation. aa_width controls antialiasing of the hard edges the node can manufacture - the constant-stop cliff, the wrap seam, the angular branch cut - and it's a no-op at identity settings, where the node is a bitwise plain ramp.
distribution defaults to native (the ramp's own declared range) with coverage on top; invert flips it, and width/height set the output size - ignored if you wire reference_image or reference_mask. Those two reference inputs are the trick that keeps a whole graph honest: wire the image you're going to composite onto, and the field comes out at exactly H, W and batch, so nothing downstream resizes.
linear_u, linear_v, radial and diamond are your vignette and directional-fade workhorses. box is a rectangular falloff, which is what you want for letterbox-style reveal masks. angular is a sweep around the origin - useful, and also the one with an inherent seam at the branch cut, since it flattens a circular quantity into a linear mask. It renders the seam as a correct one-pixel blend rather than hiding it, but you'll see it if you go looking.
Outputs
mask is the field - soft, normalised, ready for Threshold, Remap, Morphology or Composite. preview is the same thing replicated to three channels as an IMAGE, which saves you a Mask To Image node when you just want to look at it before committing.
Install
Manager → search ComfyUI Field, or:
cd ComfyUI/custom_nodes
git clone https://github.com/jeremieLouvaert/ComfyUI-Field
Restart ComfyUI. No models to download. The ramp canvas is a frontend widget the pack ships in its web/ directory - it's frontend-only, no server routes - so if the node loads but shows only a JSON box with no draggable strip, hard-refresh the browser (Ctrl+Shift+R); a stale cached frontend is the usual culprit, not a broken install.
Trap worth naming
A constant segment is a plateau, so a coverage setting that lands inside its mass will step across it. That's shared behaviour with every histogram-based control in the pack - plateaus are effectively atoms. If your coverage slider seems to jump, check whether the ramp has a flat segment sitting under the threshold.
Inputs (16)
| Name | Type | Default | Description |
|---|---|---|---|
| mode | COMBO | linear_u | Ramp shape |
| center_x | FLOAT | 0.50-1–2 | Ramp origin, window fraction (0.5 = frame centre) |
| center_y | FLOAT | 0.50-1–2 | Ramp origin, window fraction (0.5 = frame centre) |
| rotation | FLOAT | 0-360–360 | Degrees, counter-clockwise. No effect on radial (rotation-invariant) |
| ramp | STRING | {"version": 1, "stops": [{"p": 0.0, "v": 0.0, "i": "linear"}, {"p": 1.0, "v": 1.0, "i": "linear"}]} | Stops ramp: {"version":1,"stops":[{"p":,"v":,"i":}]}, i in constant/linear/smooth. Edit via the canvas widget or paste JSON directly (docs/field-phase2c-derivation.md 2.5) |
| repeat | FLOAT | 1.01–32 | Tiling repeats across the declared range |
| mirror | BOOLEAN | false | Triangle-fold instead of hard wrap, repeat > 1 |
| phase | FLOAT | 0.00-2–2 | Slides the ramp before wrapping |
| aa_width | FLOAT | 1.00–4 | Antialias width, pixels. Active when repeat>1, phase!=0, centre!=default, mode=angular, or the ramp has a value discontinuity interior to the achieved range |
| distribution | COMBO | native | native: field's own declared range. uniform: probe-grid PIT, exact coverage |
| coverage | FLOAT | 0.500–1 | Fraction of frame above midpoint. 0.5 is a no-op |
| invert | BOOLEAN | false | — |
| width | INT | 51216–8192 | — |
| height | INT | 51216–8192 | — |
| reference_imageopt | IMAGE | — | |
| reference_maskopt | MASK | — |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| mask | MASK | — |
| preview | IMAGE | — |