Renderer2D

Live 2D rendering from code. GLSL or JavaScript Canvas2D, your choice.

A node that renders code to an image, live. Connect it to a Text node holding GLSL or JavaScript, and the output texture updates as you type. Animate with a global time clock.

Setup#

  1. Add a Text node. Set its language to GLSL or JavaScript.
  2. Add a Renderer2D node next to it.
  3. Wire Text → Renderer2D's code input.

The Renderer2D's title pill shows the active language as a badge. Change languages by switching the Text node's language dropdown.

void main() {
    writeOutput(vec4(uv, 0.5 + 0.5 * sin(iTime), 1.0));
}

Press Play in the toolbar to start the time clock; the output animates.

GLSL Mode#

Two signatures are accepted. Pick whichever fits the snippet:

// Short form, most ergonomic for hand-written code.
void main() {
    writeOutput(base(uv));
}
// Shadertoy form, paste any single-pass Shadertoy snippet as-is.
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    fragColor = base(uv);
}

Both forms have access to the same built-ins.

Built-ins#

Name Type What it is
uv vec2 Pixel position normalised to 0..1 (precomputed from fragCoord / iResolution.xy)
iTime float Seconds since playback started
iResolution vec3 (width, height, pixelDensity)
iMouse vec4 Always vec4(0) today, kept declared so Shadertoy snippets that reference it still compile
writeOutput(c) macro Writes c to the output. Equivalent to fragColor = c in the Shadertoy form.

Sampling input images#

Declare additional ports with directives at the top of the source. Each directive becomes a port on the Text node holding the source. Connect upstream nodes there, and the values flow through to Renderer2D over the same code wire:

// @input image base
// @input float amount 0.0..1.0 = 0.5
// @input color tint = #ff8040

void main() {
    vec4 c = base(uv);
    writeOutput(mix(c, tint, amount));
}

Image inputs become a sampling macro of the same name. Numeric inputs become typed locals. One Text node can fan out to several Renderer2Ds. They all see the same bindings without duplicate wiring.

Built-ins#

Helpers compiled into every shader so you don't reimplement them.

Noise

Function Returns Notes
noise(vec2 p) float 2D value noise, 0–1
fbm(vec2 p, int octaves) float Fractal brownian motion
hash21(vec2) float Pseudo-random hash

Color

Function Notes
palette(t, a, b, c, d) Cosine palette
hsv2rgb(vec3) / rgb2hsv(vec3) Convert

SDF shapes (negative inside, positive outside)

Function Notes
sdCircle(p, r) Circle
sdBox(p, size) Rectangle
sdRoundedBox(p, size, r) Rounded rectangle
sdLine(p, a, b) Line segment
smin(a, b, k) Smooth blend of two shapes

Combine: min(d1, d2) for union, max(d1, d2) for intersection, smin for smooth blend.

Utility

Function Notes
rotate2d(angle) 2D rotation matrix
remap(v, inMin, inMax, outMin, outMax) Range remap
saturate(x) Clamp 0–1

Constants: PI, TAU, PHI, E.

JavaScript Mode (Canvas2D)#

Set the Text node's language to JavaScript and write standard Canvas2D imperative drawing. The runtime gives you a ctx (a CanvasRenderingContext2D-shaped wrapper), plus width, height, and time. By default, the whole code body runs once per frame:

ctx.fillStyle = "rgb(20, 25, 40)"
ctx.fillRect(0, 0, width, height)

ctx.save()
ctx.translate(width / 2, height / 2)
ctx.rotate(time * 0.5)
ctx.fillStyle = "rgba(255, 80, 100, 0.85)"
ctx.fillRect(-60, -60, 120, 120)
ctx.restore()

Stateful sketches with main()#

Need state that persists across frames? Define a main function. Top-level code then runs once on (re)compile and main runs every frame, closing over your initial state:

let trail = []

function main(ctx, width, height, time) {
    trail.push({ x: width / 2 + Math.cos(time) * 80, y: height / 2 + Math.sin(time) * 80 })
    if (trail.length > 60) trail.shift()

    ctx.fillStyle = "rgb(15, 10, 30)"
    ctx.fillRect(0, 0, width, height)
    for (let i = 0; i < trail.length; i++) {
        ctx.globalAlpha = i / trail.length
        ctx.fillStyle = "rgb(255, 100, 200)"
        ctx.fillRect(trail[i].x - 4, trail[i].y - 4, 8, 8)
    }
    ctx.globalAlpha = 1
}

Editing the code resets the state (top-level re-runs). Predictable and matches the rest of the editing loop. main can declare any subset of (ctx, width, height, time, ...bindings) as parameters; what you don't declare is still reachable via the outer scope, but won't update per frame.

Most of the browser's Canvas2D API works:

  • State: save, restore, with full property snapshot
  • Transforms: translate, rotate, scale, transform, setTransform
  • Rectangles: fillRect, clearRect, strokeRect
  • Paths: beginPath, moveTo, lineTo, arc, arcTo, quadraticCurveTo, bezierCurveTo, rect, ellipse, closePath
  • Path drawing: fill() (with fillRule), stroke() (miter joins, butt caps), clip() with state-stack-aware mask
  • Images: drawImage (3/5/9-arg)
  • Gradients: createLinearGradient, createRadialGradient, createConicGradient + addColorStop; gradient or color string fillStyle/strokeStyle
  • Pixel access: getImageData, createImageData
  • Composition: globalAlpha, globalCompositeOperation (blend modes)

Inputs via directives#

The // @input syntax works in JavaScript too. Each directive declares an extra port on the Text node and exposes a same-named variable to your JS code:

// @input image base
// @input float amount = 0.5

ctx.drawImage(base, 0, 0, width, height)
ctx.fillStyle = `rgba(255, 0, 0, ${amount})`
ctx.fillRect(0, 0, width, height)

Image inputs are shaped like { textureId, width, height }, exactly the form ctx.drawImage accepts. Numeric inputs come through as plain JS values (float → number, vec2/3/4{x,y[,z[,w]]}, color{r,g,b,a}).

Not yet supported#

These warn once and degrade gracefully:

  • fillText / strokeText / measureText
  • shadowColor / shadowBlur / shadow offsets
  • Pattern fills (createPattern records the source but isn't applied)
  • imageSmoothingQuality non-default
  • Round / bevel / square line caps and joins (defaults to butt + miter)

Output Size#

Fixed at 480×360 logical pixels. The texture renders at 2× backing for sharp edges on retina displays. Your code keeps drawing in the 480×360 logical space without thinking about device pixels.

Next#