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#
- Add a Text node. Set its language to GLSL or JavaScript.
- Add a Renderer2D node next to it.
- 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()(withfillRule),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/measureTextshadowColor/shadowBlur/ shadow offsets- Pattern fills (
createPatternrecords the source but isn't applied) imageSmoothingQualitynon-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#
- User Library: saving and reusing brushes