Nodes/GIF Toolkit/GIF Multi-Text Designer
ComfyUI Node

GIF Multi-Text Designer

Three text layers you drag around with the mouse

By oekobudde·Created 28 days ago·Updated 26 days ago· 1
GIF Multi-Text Designer
  • images
  • style
  • preview
◄enabledtrue►
◄layers_json[{"enabled":true,"text":"LET'S GO!","font":"DejaVuSans-Bold.ttf","font_size":16,"font_color":"#40e704","x_percent":45.2,"y_percent":5.6,"background_enabled":false,"background_color":"#000000","background_opacity":160,"padding":8,"corner_radius":8,"outline_enabled":true,"outline_color":"#000000","outline_width":2,"shadow_enabled":true,"shadow_color":"#000000","shadow_offset_x":2,"shadow_offset_y":2,"line_spacing":4},{"enabled":false,"text":"","font":"DejaVuSans-Bold.ttf","font_size":20,"font_color":"#ffffff","x_percent":15.0,"y_percent":85.0,"background_enabled":false,"background_color":"#000000","background_opacity":160,"padding":8,"corner_radius":8,"outline_enabled":true,"outline_color":"#000000","outline_width":2,"shadow_enabled":false,"shadow_color":"#000000","shadow_offset_x":2,"shadow_offset_y":2,"line_spacing":4},{"enabled":false,"text":"","font":"DejaVuSans-Bold.ttf","font_size":20,"font_color":"#ffffff","x_percent":85.0,"y_percent":85.0,"background_enabled":false,"background_color":"#000000","background_opacity":160,"padding":8,"corner_radius":8,"outline_enabled":true,"outline_color":"#000000","outline_width":2,"shadow_enabled":false,"shadow_color":"#000000","shadow_offset_x":2,"shadow_offset_y":2,"line_spacing":4}]►
◄active_layer0►
◄font_catalogDejaVuSans-Bold.ttf►
◄preview_frame0►

Caption placement by number is miserable. You type y_percent: 15, run the whole graph, wait, look at the picture, guess again. The Multi-Text Designer exists to kill that loop: it puts your actual frame on the node, renders the text on top of it, and lets you push the text around with the cursor until it looks right.

It's the reason people install this pack. Everything else here is a helper around it.

How it works

Structurally it's two halves. The Python half is a normal node: you feed it an images batch, it picks one frame, converts it to a PIL image, draws the enabled text layers onto it, and returns both the rendered preview and a style object describing the layers. The browser half is a JavaScript extension that draws that preview onto a canvas on the node body, handles hit-testing so a click on visible text selects that layer, and writes the new position back into the node's data when you stop dragging.

Two details make it hold together across resolutions. Positions are stored as percentages of the frame, not pixels, so a layer placed at 45%/6% on a 320-pixel-wide preview lands in the same relative spot on any output size. And the saved JSON - the string in layers_json - is what the final export renders from. The same Python function paints the preview and the exported frames, so what you saw really is what you get. There's no separate render path to drift.

One thing you'll feel: the node is deliberately always-dirty (IS_CHANGED returns NaN) so the preview refreshes on every Run. That's the standard ComfyUI idiom for "must run every time," and it costs you the cache behind it - a GIF Toolkit graph re-renders its source frames each Run. Worth knowing before you conclude the pack is slow.

The inputs and output that matter

  • images (IMAGE) - the prepared frame batch. Comes from GIF Prepare / Preset.
  • enabled (BOOLEAN, default true) - global master switch for all text. Off means nothing renders and frames pass through untouched; no bypassing nodes by hand.
  • layers_json (STRING, multiline) - all three layers, as JSON. The canvas edits it; you can edit it by hand if you want exact numbers, and it's readable enough to do that.
  • active_layer (INT, 0–2) - which layer the style controls and the drag apply to.
  • font_catalog - the font, picked from a list built by scanning your machine's font directories.
  • preview_frame (INT) - which frame of the batch is shown.

Outputs: style (GIF_TEXT_STYLE), which wires into GIF Text Overlay to actually burn the text into the whole batch, and preview (IMAGE), a single rendered frame you can send to a Preview Image node. The node is an output node, so it runs every time.

Layer count is exactly three - no adding a fourth.

Using the UI

Click Layer 1 / 2 / 3 to select one; the selected tab is highlighted gold, and clicking visible text on the canvas selects its layer too. Then type, style, and drag. The 3×3 grid button row is the fast way to snap something to a corner. Duplicate → next copies the active layer into the next slot with a small offset, which is how you build a matching second line without retyping the style. Opening Advanced style reveals outline width, padding, corner radius, background opacity, shadow offsets, and line spacing, and the node grows to fit.

Install

Manager → search GIF Toolkit → restart. Or:

cd ComfyUI/custom_nodes
git clone https://github.com/oekobudde/ComfyUI-GIFToolkit.git

No pip dependencies from this pack, but the workflow needs ComfyUI-VideoHelperSuite for loading video. Restart ComfyUI fully - the canvas extension registers at startup, so a browser refresh alone won't bring the drag UI up.

Common issues

Black background behind the text before your first Run. Known and intentional. The designer draws a black frame with your text on it until real frames arrive; Run once and the source image appears behind it.

Text won't drag. Three things to check, in this order: enabled is on, the selected layer is enabled, and the layer actually contains text. Empty text has nothing to grab.

Layer 2 or 3 never shows up. They ship disabled. Select the layer, then hit Enable selected layer.

The font dropdown doesn't have the font a shared workflow used. The list is generated from your system fonts at import - C:/Windows/Fonts, /usr/share/fonts, ~/.fonts, the macOS font folders. A workflow made on someone else's machine can name a font you don't have. Re-pick the font on your node; if the name can't resolve, the renderer quietly falls back to the PIL default rather than crashing.

CategoryGIF Toolkit/Text

Inputs (6)

NameTypeDefaultDescription
imagesIMAGE—
enabledBOOLEANtrue—
layers_jsonSTRING[{"enabled":true,"text":"LET'S GO!","font":"DejaVuSans-Bold.ttf","font_size":16,"font_color":"#40e704","x_percent":45.2,"y_percent":5.6,"background_enabled":false,"background_color":"#000000","background_opacity":160,"padding":8,"corner_radius":8,"outline_enabled":true,"outline_color":"#000000","outline_width":2,"shadow_enabled":true,"shadow_color":"#000000","shadow_offset_x":2,"shadow_offset_y":2,"line_spacing":4},{"enabled":false,"text":"","font":"DejaVuSans-Bold.ttf","font_size":20,"font_color":"#ffffff","x_percent":15.0,"y_percent":85.0,"background_enabled":false,"background_color":"#000000","background_opacity":160,"padding":8,"corner_radius":8,"outline_enabled":true,"outline_color":"#000000","outline_width":2,"shadow_enabled":false,"shadow_color":"#000000","shadow_offset_x":2,"shadow_offset_y":2,"line_spacing":4},{"enabled":false,"text":"","font":"DejaVuSans-Bold.ttf","font_size":20,"font_color":"#ffffff","x_percent":85.0,"y_percent":85.0,"background_enabled":false,"background_color":"#000000","background_opacity":160,"padding":8,"corner_radius":8,"outline_enabled":true,"outline_color":"#000000","outline_width":2,"shadow_enabled":false,"shadow_color":"#000000","shadow_offset_x":2,"shadow_offset_y":2,"line_spacing":4}]—
active_layerINT00–2—
font_catalogCOMBODejaVuSans-Bold.ttf48 options: PIL Default, DejaVuSansMono-Bold.ttf, DejaVuSerif.ttf, DejaVuSansMono-Oblique.ttf, DejaVuSansMono.ttf, DejaVuSerif-Bold.ttf, +42
preview_frameINT00–9999—

Outputs (2)

NameTypeDescription
styleGIF_TEXT_STYLE—
previewIMAGE—