I procrastinated on making art by building a tool for making art. Of course, it got out of hand. Or rather, as in this example, it got inside the hands:

You can try playing with the effect using your own webcam by clicking “Try yourself” above. Fully in your browser!

You are looking at an output of Kabelsalat, a browser-native node-based workflow editor for image and video effects and interactive visuals. I also did a cute little launch video, using the /brag skill:

Open kabelsalat.studio Needs a desktop browser with WebGPU.

Pixelsuppe, reheated

A few years ago I wrote what I then called Pixelsuppe (pixel soup), a Python project where image and video effects were pieces of code that could be plugged together easily. Nothing too crazy, I just wrapped Pillow images and video frames, added transforms for things like pixel sorting and masks, and some helpers to make it easy to combine the pieces in scripts. Running a script applied the effects to an input image or video and saved the output.

Lately, I wanted to get into creative coding a bit more, so I decided to redo that project, but to do it properly: fully in-browser, with an intuitive UI and support for interactive art. Another evergreen motivation of mine: wanting to push the current AI coding models and see what I could build with them, which is my way of keeping up with the Claudashians, so to say.

At some point it was sufficiently different from what I had built before that I needed a new name. Considering there are a lot of cables on the screen when using this app, I went with Kabelsalat. Literally “cable salad”, the German word for that tangled mess behind your desk. At least the cables in my app have an undo button.

A first look

I already liked the feeling of connecting nodes in ComfyUI, so I adapted it (ComfyUI’s LiteGraph implementation, to be precise) and built an image-processing playground around it:

Double-click, search, connect: a Refraction lens goes between a photo and the Output. Its node preview compares before and after while I drag Strength.

A node takes some input, does something to it, and offers the result at an output socket. Connect that socket to another node and its result becomes the next node’s input. A collection of connected nodes (an acyclic graph) is a workflow.

Here’s a slightly more artsy use: the Terminal dancer example turns a dance clip into green terminal characters.

Input clip

Matte from the portrait model

Terminal dancer output

The Portrait Mask node runs MODNet, a small matting model of about 6.5 million parameters. It cuts people out of photos, including full bodies despite the models description. It runs right in the browser, through Transformers.js on top of ONNX Runtime Web, using the GPU via WebGPU when available.

The model outputs a matte: white for the dancer, black for the background, and shades of gray along soft edges. The full workflow below shows how that mask is used: Apply mask keeps only the dancer’s pixels from each input frame, and Glyph raster replaces each little cell of the remaining image with an ASCII character, picking dense ones like @ and # for bright cells and sparse ones like . and : for dark ones. The characters then land on a dark green background. Bish bash bosh, hacker aesthetics.

Kabelsalat editor showing the Terminal dancer workflow in two groups: Dancer clip, Find the dancer, Bake the matte, Keep only the dancer and the Extracted matte output on top; Dark terminal screen, Terminal glyphs, Characters on screen and the Terminal dancer output below. Two small floating previews at the bottom left show the white matte and the green ASCII dancer
The Terminal dancer workflow in Kabelsalat. The colored groups are just for tidiness. Bottom left: the extracted matte and the final output.

Now make it move: Signal nodes

You can drive node settings from anything that outputs a value, for example a Sine generator node. These “signal” nodes produce a value that depends on the current time and on their own settings (which you, if you’re adventurous, can of course drive with other signal nodes, too).

The simplest one is the Sine node. By default it swings between −1 and 1 once per second, which happens to be exactly the range of the twirl Strength in Lens warp:

A Sine node wired into the Strength of Lens warp.

Example: Liquid displacement

The Liquid displacement example takes this a little further. It generates two noise fields. A noise field is just a grayscale image of smooth, cloudy random values, like a height map of rolling hills. The first field is mapped to colors with a Gradient map node. The second noise field (Moving displacement in the workflow screenshot below) decides how far, and in which direction, each pixel of the colored image gets pushed around. Two Sine signals slide that second field in a slow circle, one full lap every ten seconds, so the animation loops seamlessly, which tickles my brain.

Kabelsalat editor with the Liquid displacement workflow in three groups: Color body (noise field, mask to image, gradient map), Ten-second orbit (two sines driving a second noise field, previewed through Mask to image), and Displace, with four output panes below showing gray noise, colored noise, the gray displacement field, and the displaced result
Liquid displacement in the editor. The four outputs show the noise, the colored noise (after the gradient map), the moving displacement field, and the final displaced result.

Signals can also come from other places:

  • More waveforms: Triangle, Saw, and Square, plus Random for repeatable randomness.
  • Keyframe curves: for more deliberate timing, with hold, linear, or eased interpolation and looping support.
  • The clock itself: You can easily get the workflow time in seconds, or alternatively the current frame number.
  • Audio features: You can extract loudness, bass, treble, or the beats. So the bass energy of a track can drive your workflow to make for audio-reactive effects. You can also extract features from a microphone input.
  • External input nodes: MIDI controllers (making use of the excellent controller mappings from Mixxx) and also gamepads, in my case a Stadia controller.
  • Code: a p5.js Signals node computes values from JavaScript, if that’s your jam.

Then there are some math nodes to Add, Multiply, Map Range, or Clamp your signals.

Videos, and a bit of time travel

Videos ride on the same clock as the signals. A Load video node hands out the frame at the current playhead, and its Playhead offset is just another setting with a socket, so you can wire a value into it and shift the video in time.

The Chromatic echoes example abuses exactly that. It loads the same dance clip three times. One Control node holds the echo spacing (0.24 seconds), and two Multiply nodes turn it into −0.24 and −0.48 seconds, so the second and third copy run slightly behind the first. Then each copy gets its own color: a Gradient map paints its dark tones black and its bright tones yellow, cyan, or magenta. Finally, two Blend nodes in Screen mode stack the three copies back together. Where nothing moves, the inks add up to plain gray tones again. Where the dancer moves, we get a nice visual:

Kabelsalat editor with the Chromatic echoes workflow in three groups: One clip, three moments (three Load video nodes, an Echo spacing control and two Multiply nodes feeding the playhead offsets), Tint the light (yellow, cyan and magenta gradient maps), and Overlap the colors (two Screen blends and the Output), with the colorful echoes and the original clip in two small floating outputs
The Chromatic echoes workflow. The orange wires carry values: the echo spacing, multiplied into two playhead offsets.

Original

Chromatic echoes output

Input and output side by side.

How the example at the very top of the page was made

As for the interactive example at the top, it uses Google’s MediaPipe Hand Landmarker, a hand-tracking model of about 8 MB. For every webcam frame, it finds landmarks: 21 named points per hand, from the wrist to each fingertip, with their positions in the image. Let’s have a look:

A woman framing her face with both hands; cyan crosshairs and lines mark 21 points on each hand, along every finger
The landmarks the model finds in one frame, drawn with a HUD overlay node that I added for debugging purposes.

From the detected thumb and index tips of both hands, I can generate a mask using a Landmarks to mask node. I also blur its edges a little; I think that makes for a nicer effect.

Part of the Finger window workflow in the editor: Find fingertips lists 2 hands with 21 points each, Four-finger polygon shows the camera frame next to the polygon mask, Soften the window edge shows the blurred mask, followed by Feathered mask preview and an Output
The mask branch of the Finger window workflow.

The input to the Find fingertips node is just the camera video. But the camera image also takes a second route, to generate the version inside the four fingertips: a Displacement node pushes its pixels around guided by a slowly drifting noise field, and a Gradient map recolors the result in pink and violet. A final blend uses the fingertip mask to put that Effect image over (a gently color-graded version of) the camera feed.

There are also nodes to turn landmark coordinates, distances, and joint angles into numbers. The gap between thumb and index finger can control Pixel Sort’s Amount, for instance. A raised arm can change a parameter. The same sort of wiring works with MIDI controls and gamepad sticks, so I can turn a workflow into something I actually play with.

Hand tracking is one of seven specialized vision models in Kabelsalat, all running fully in your browser:

  • Portrait mask (MODNet): separates a person from the background.
  • Object selection (SlimSAM): click a few points or draw a box, and get a mask of that object.
  • Background removal (BiRefNet Lite): a cutout for anything in the foreground, not just people.
  • Depth map (Depth Anything V2 Small): guesses how far away every pixel is. Fun in a red/blue anaglyph workflow for a fake 3D effect.
  • Pose, hand, and face landmarks (MediaPipe): especially the face landmarks are super dense and detailed.

How does this work under the hood?

To render an Output node, the rendering engine follows its connections backwards to find the inputs it needs. A workflow is a directed acyclic graph: nodes connected by one-way edges, with no loops allowed. Without circular dependencies, the renderer can always arrange the work such that each node’s inputs are ready before that node runs.

Planning the Terminal dancer render from its Output Dancer clip Step 1 Portrait mask Step 2 Screen color Step 5 Apply mask Step 3 Glyph raster Step 4 Composite Step 6 Output Step 7
Walking back from Output finds everything the Terminal dancer needs, and derives an execution order from the dependencies.

All of that runs in a Web Worker, a separate JavaScript thread that can’t touch the page and only talks to it by passing messages back and forth. That way, while the worker prepares frames and sends work to the GPU, the editor stays responsive, preventing a heavy render from turning the app into a slideshow.

The GPU work goes through WebGPU, the browser’s modern interface to the graphics card. I don’t use it directly, though, but through VGPU, a fairly new TypeScript library on top of it. VGPU gives me two main niceties: (a) Shader files have their inputs type-checked, so a renamed shader parameter breaks at build time instead of rendering gibberish or crashing, and (b) the same code can also run in headless Node, and against a mock GPU, allowing for better tests (proper CI/CD testing without a GPU!) and quicker iteration because you(r agents) don’t have to start a browser to render a workflow.

Most image effects are implemented as WGSL shaders (small programs for the GPU). A shader reads input textures (~images) and writes an output texture. For something like a color adjustment, lots of pixels can be processed in parallel. The texture one effect produces becomes the next effect’s input directly on the GPU, without bringing all its pixels back into JavaScript between steps. Only the finished output is copied into an ImageBitmap and sent back to the editor, which draws it into the preview:

The editor, the workers, and the GPU Editor Workflow + playhead Renderer Web Worker: plans work GPU: first effect Reads input textures GPU: next effect Reads previous texture Output image Copied to an ImageBitmap Preview Editor draws the bitmap Inference worker Runs the AI models
The editor (the page you click around in) hands the workflow and the current playhead to the renderer, which runs in its own Web Worker. The GPU runs one effect after another, each reading the previous texture. The finished output comes back as an ImageBitmap, and the editor draws it into the preview.

The AI models run in a separate inference worker, so the renderer has to copy the input frame off the GPU into ordinary memory, send it over, and wait for the result to come back. I didn’t figure out a way to keep it all on the GPU, which would spare that round trip, but thanks to caching and baking (see the next section), the AI models really only have to run once per image or video they process.

Making it fast

My favorite way to use Kabelsalat is to build an interactive workflow, project it onto a wall, and interact with it using my controller. That only works if the picture keeps up with my thumbs, so the renderer has to maintain a high FPS by rendering frames as fast as possible.

And since the fastest render is the one that never happens, caching is the obvious move. For each node’s result, the renderer remembers what went into it: its parameter values and inputs. When a later render needs that node with the same ingredients, it reuses the stored result instead of computing it again. For example, when I change the glyph color to blue, this is what happens:

Changing only the glyph color Dancer clip Reuse Portrait mask Reuse Screen color Reuse Apply mask Reuse Glyph raster New color Composite Render again Output Render again From the cache Renders again
I switched the ASCII glyphs color from green to blue. The clip, the matte, the cutout, and the screen color do not depend on that color, so they come from the cache. Only the glyphs and everything after them render again.

The easy (well, easier) part of a cache is the rule for when something is reusable. The hard(er) part is deciding which cached results walk the plank once the memory fills up: you need a cache eviction strategy. Classic choices are least recently used (throw out whatever hasn’t been needed for the longest time) and least frequently used (throw out whatever has been needed the fewest times). Smarter strategies also ask how long a result would take to compute again, and how much memory it takes up.

For Kabelsalat, after arguing with Opus for a while, I went with Greedy-Dual-Size-Frequency, which does all of that. A result’s value is roughly how often it gets reused, times how long it would take to recompute, divided by the memory it occupies. The renderer measures those times as it runs, and a result also gets credit for the work of the upstream nodes it saves. Works like a charm!

There are three main other features that help the responsiveness, too:

  • Draft preview: reduces the output resolutions for experimenting; exports stay in full quality.
  • Frame cache: while playback is paused, the renderer pre-renders the next frames and fills the frame cache. A green strip under the timeline shows which frames are currently cached: The timeline: play controls, the playhead, and green bars under the scrubber marking the cached frame ranges
  • Baking: a Bake node goes one step further. Put it behind an expensive branch, and Kabelsalat renders that branch for a whole frame range once and stores every frame losslessly in the browser storage for later reuse. Why “baking” and not just more caching? The caches above work automatically, live in limited memory, and forget things whenever they need room. A bake is something you ask for, it covers the whole range up front, and it stays in the browser’s storage until you delete it. I use it, for example, to run a slow AI model over a whole video once, up front: the Terminal dancer example bakes its matte that way.

The assistant

To make the process of fooling around with some artsy ideas as smooth as possible, I decided to add a chat assistant. It knows the node catalog and can build and edit workflows for you:

A demo request to the assistant, fast-forwarded while the model thinks and looks up nodes. The proposal arrives as a card; Add to current workflow opens a map for placing the new nodes.

Like the rest of the app, the assistant has no backend either: the browser talks to OpenRouter directly, using the Vercel AI SDK for streaming and tool calls. You bring your own OpenRouter API key and pick any model from their catalog (well, it needs to support tool calls).

I (well, we..) managed to make this work really well, even when using something as cheap as deepseek-v4.1-flash as the model. The hard part was that with 130 nodes, all with extensive parameter descriptions, you need to be a bit careful about how you build the prompts sent to the model, and you need to design the tools accordingly. Here is how the assistant is implemented and what it can do:

  • The system prompt has just a node index: it lists every node’s type and title, plus the essential rules of the workflow format. In total, its well under a thousand tokens for all 130 nodes.
  • Tool calls: the model can call a tool to fuzzy-search the node catalog, or another tool to look up the exact details (input/output ports, parameter ranges, and defaults) of up to four nodes at once. It can also inspect the workflow you attached in a token-efficient summary.
  • Validation tool: to propose a workflow, the model hands a complete workflow to one more tool call. If there is an issue (e.g. the model connected an image output to a value input), it gets the errors back and one chance to fix them.
  • Explicit context: the current workflow is attached by default, and you can add selected nodes, a snapshot of an output, or even a few seconds of the timeline rendered to video (if the model supports native video input). This is the assistant’s “Edit context” panel: The assistant's Edit context panel: Current workflow ticked, then Attach selected nodes and Attach output under From the canvas, and Image, Audio and Video under Reference files, with Audio and Video marked unsupported for the selected model
  • Review-then-apply: the model never touches the canvas directly. It proposes a workflow, and you decide: open it in a new tab, replace the current workflow with it, or add the proposed nodes to the current workflow: A proposal card titled Breathing kaleidoscope from noise field, with counts for nodes, links, added, adjusted and removed, and buttons Replace current workflow, Add to current workflow and Open in new tab
  • Cost tracking: the assistant keeps track of what every chat costs. After each request, it adds the tokens used and the cost that OpenRouter reports for that request to the chat’s tally. Settings → Assistant → Usage then lists all chats saved in the browser. My kaleidoscope chat from above cost less than a cent ($0.0079, which the table rounds up): The Chat usage table in Settings: five chats with their workflow, input tokens, output tokens and cost, from Breathing kaleidoscope from noise field at 39,412 input tokens and $0.01 to Make the glyphs pulse with the bass at $0.07, and a total of $0.16 for five chats
How the assistant builds a workflow You Prompt + context Assistant panel Runs in your browser OpenRouter Using your API key Tool calls Search, look up, inspect Validation Parser + node registry Review card New tab, replace, or add
Only the orange box is outside your browser: OpenRouter forwards the conversation to the model you picked. The tool calls and the validation run locally, and nothing reaches your workflow until you pick an action on the review card.

Random collection of cool features

  • p5.js support: a p5.js Canvas node runs a sketch (optionally reading an input image), and the result is fed back into the workflow; a p5.js Signals node produces values that can control other nodes. Variables in the code can be exposed as node inputs with special comments.
  • WGSL shaders: the WGSL Shader node runs a fragment shader you write yourself, with up to four images and two masks as inputs.
  • Portable workflows: workflows can be exported to JSON files. Exported image outputs also carry the workflow that produced them in their metadata, so you can drop one back into Kabelsalat to recover the workflow. Alternatively, the .kabelsalat project files also include the media needed for a workflow, for maximum portability.
  • Custom hotkeys: every command is listed under Settings → Hotkeys and can be rebound, and Ctrl/⌘+K opens a command palette. It’s a pet peeve of mine when apps don’t let you customize hotkeys, so this had to be in.
  • Tabs and undo: several workflows can be open side by side in tabs, each with its own undo/redo history. A dot marks tabs with unsaved changes. Everything autosaves in the browser: The Kabelsalat top bar with five workflow tabs: Terminal dancer, Liquid displacement, Chromatic echoes, Finger window and Flower kaleidoscope; Liquid displacement and Flower kaleidoscope show a dot for unsaved changes
  • Multiple output nodes: they can be independently zoomed/panned and also detached into another browser window (useful for projecting onto a wall).
  • Fractals: Mandelbrot, Julia, Burning Ship, and a few more exotic formulas too. Mandelbrot goes down to a zoom of 10¹². Look, this is rendering live in your browser right now, as in, it’s not a video!
Four outputs from Fractal lab: deep Mandelbrot detail, Julia, Burning Ship, and a Julia mask composition
A few views from Fractal lab. Apparently I also needed somewhere to zoom into fractals.

Limitations

Kabelsalat is built to be fun to use, with an intuitive UI and a low barrier to entry for newcomers to creative coding. If you are looking for something super polished and ready for professional VJing, you are better off using something like Isadora or TouchDesigner.

To be more specific, here are some limitations and things you might miss:

  • Needs working WebGPU: The interface is desktop-first, and support for video codecs and MIDI access varies by browser. Large workflows and models can exhaust the available memory, and frame rates heavily depend on your machine.
  • No projection mapping: self-explanatory.
  • No pro video plumbing: nothing goes in or out over NDI, Spout/Syphon, OSC, or DMX. Live inputs are limited to whatever the browser offers: cameras, screen capture, audio, MIDI, and gamepads.
  • No 3D: there are no meshes, cameras, materials, or lights. The p5.js Canvas node only draws in 2D, and the depth-based nodes (Depth parallax, Depth relight, the red/blue anaglyph) fake depth from a flat image rather than building a 3D scene. If you’re stubborn, you can raymarch a scene inside the Shader node, but nothing in the workflow will help you with it.
  • Performances can’t be replayed: you can record the live output to a video file, but not the performance itself. The camera feed, MIDI knobs, and gamepad presses aren’t saved as inputs you could replay later, so you can’t, say, render last night’s jam again in 4K with a tweaked color grade. What you recorded is what you get.

That being said, I am happy with how this turned out, and thoroughly impressed with the latest models that helped me build this (Anthropic’s Opus 5.5 and OpenAI’s GPT-6-Astra). And besides that, this is just procrastination! Now let me get back to the things I am procrastinating on, which are… more personal projects.

That’s all!

Try Kabelsalat and start with one of the example workflows. If you wanna hack on it, the source code is here.