Snippet node allows to write custom JavaScript to procedurally generate or modify geometry. It acts as an execution sandbox, providing a specialized API for interacting with underlying data that appears on canvas.
Core workflow
Writing a snippet usually follows a logical four-step process: generating shapes, modifying their transforms, styling their appearance, and outputting them.
Generate
Use create API to instantiate new geometric shapes. You can pass an object with standard properties (like width, height, or radius) to define the initial geometry.
let rect = create.rectangle({ width: 100, height: 100 });let rect = create.rectangle({ width: 100, height: 100 }); Modify
Once a shape is created, you can use chainable methods to adjust its spatial properties.
rect.translate(50, 20).rotate(45).scale(1.5, 1.5);rect.translate(50, 20).rotate(45).scale(1.5, 1.5); Style
Apply fills, strokes, opacity, blurs, shadows, and blend modes using chainable methods.
rect.fill({ color: '#ff0055', opacity: 0.8 })
.stroke({ color: '#ffffff', width: 2, dash: [4, 4], align: 'inside' })
.shadow({ color: '#000', blur: 15, offset: { x: 5, y: 5 }})
.opacity(0.5);rect.fill({ color: '#ff0055', opacity: 0.8 })
.stroke({ color: '#ffffff', width: 2, dash: [4, 4], align: 'inside' })
.shadow({ color: '#000', blur: 15, offset: { x: 5, y: 5 }})
.opacity(0.5); Output
Finally, output the shape so it can be rendered or processed by downstream nodes.
output.add(rect);output.add(rect); Warning: By default, any shape passed into the Snippet node flows right through it. However, if you explicitly call output.add(myShape), auto-passthrough is disabled. If you want to keep the incoming shapes alongside your new ones, you must manually add them via output.add(...inputs.shapes, myShape) spread syntax.
Using Built-in Nodes in Code
You can invoke any visual node’s logic directly inside your snippet using node() function. This gives you exact 1:1 parity with the nodegraph, allowing to use complex operations like Boolean, Trim path, and Scatter without writing math yourself (though you could).
The node() function takes node type name, a property configuration object, and up to three input arrays (Primary, Secondary, Tertiary — just like the visual connection ports).
// Example: Performing a Boolean Intersection
let box = create.rectangle({ width: 200, height: 200 });
let circle = create.ellipse({ radiusX: 100, radiusY: 100 }).translate(50, 50);
let result = node('boolean', { operation: 'intersection' }, [box], [circle]);
output.add(...result);// Example: Performing a Boolean Intersection
let box = create.rectangle({ width: 200, height: 200 });
let circle = create.ellipse({ radiusX: 100, radiusY: 100 }).translate(50, 50);
let result = node('boolean', { operation: 'intersection' }, [box], [circle]);
output.add(...result); Dynamic parameters
When you use ui.number(), ui.color(), or ui.toggle() inside code, the node automatically creates UI controls in the Parameters Panel.
These parameters allow to tweak visual results without having to dig back into the code.
// create number input with min 10 and max 100, and color input
let radius = ui.number('Radius', 50, 10, 100);
let color = ui.color('Dot Color', '#ff0055');
let circle = create.ellipse({ radiusX: radius, radiusY: radius });
circle.fill(color);
output.add(circle);// create number input with min 10 and max 100, and color input
let radius = ui.number('Radius', 50, 10, 100);
let color = ui.color('Dot Color', '#ff0055');
let circle = create.ellipse({ radiusX: radius, radiusY: radius });
circle.fill(color);
output.add(circle); Modifying upstream data
Any shape passed into the Snippet node is available in the inputs.shapes array. If these shapes are modified, they will be passed down to the next node (assuming no output.add() used).
// Offset the Y position of every incoming shape based on animation time
inputs.shapes.forEach((shape, index) => {
let wave = Math.sin(time + index) * ui.number('Wave Height', 20);
shape.translate(0, wave);
});// Offset the Y position of every incoming shape based on animation time
inputs.shapes.forEach((shape, index) => {
let wave = Math.sin(time + index) * ui.number('Wave Height', 20);
shape.translate(0, wave);
}); Here is another simple example adding noise to a newly created rectangle using the built-in noise function:
let rect = create.rectangle({ width: 100, height: 100 });
// Use built-in noise(x, y, seed) function to jitter position
let nx = noise(time, 0) * ui.number('Jitter Amount', 50);
let ny = noise(0, time) * ui.number('Jitter Amount', 50);
rect.translate(nx, ny);
output.add(rect);let rect = create.rectangle({ width: 100, height: 100 });
// Use built-in noise(x, y, seed) function to jitter position
let nx = noise(time, 0) * ui.number('Jitter Amount', 50);
let ny = noise(0, time) * ui.number('Jitter Amount', 50);
rect.translate(nx, ny);
output.add(rect); Generative art example
The Snippet node is powerful enough to run complex algorithms, such as Space Colonization or branching systems. Here is a complete example:
// 1. UI Controls
let maxNodes = ui.number('Max Nodes', 2000, 100, 5000, 100);
let initSeeds = ui.number('Initial Seeds', 9, 1, 50, 1);
let startRad = ui.number('Start Radius', 20, 5, 100);
let radScale = ui.number('Radius Scale', 0.95, 0.8, 1.0, 0.01);
let searchAngle = ui.number('Search Angle (Deg)', 180, 0, 360);
let maxAttempts = ui.number('Max Attempts', 15, 1, 50, 1);
let boundaryRadius = ui.number('Boundary Radius', 400, 100, 1000);
let seedVal = ui.number('Random Seed', 42, 0, 1000, 1);
let drawDots = ui.toggle('Draw Nodes', true);
let color = ui.color('Color', '#333333');
// 2. Randomness (deterministic)
let rndCounter = 0;
function getRand() {
rndCounter++;
return seededRandom(seedVal + rndCounter);
}
// emulate numpy.random.normal() using Box-Muller transform
function getNormal() {
let u = 1 - getRand();
let v = getRand();
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}
// 3. Spatial hashing grid (optimization for collision detection)
const cellSize = startRad * 2;
const spatialGrid = new Map();
function getGridKey(x, y) {
return Math.floor(x/cellSize) + ',' + Math.floor(y/cellSize);
}
function addNodeToGrid(n) {
let key = getGridKey(n.x, n.y);
if (!spatialGrid.has(key)) spatialGrid.set(key, []);
spatialGrid.get(key).push(n);
}
function getNearbyNodes(x, y) {
let gx = Math.floor(x/cellSize);
let gy = Math.floor(y/cellSize);
let nearby = [];
for(let i = -1; i <= 1; i++) {
for(let j = -1; j <= 1; j++) {
let key = (gx+i) + ',' + (gy+j);
if (spatialGrid.has(key)) nearby.push(...spatialGrid.get(key));
}
}
return nearby;
}
// 4. Initialize seed nodes
let nodes = [];
let activeNodes = []; // Nodes that can still branch
while (nodes.length < initSeeds) {
let x = (getRand() * 2 - 1) * boundaryRadius;
let y = (getRand() * 2 - 1) * boundaryRadius;
// Ensure seeds start inside the boundary circle
if (Math.sqrt(x*x + y*y) < boundaryRadius * 0.9) {
let n = {
x: x,
y: y,
r: startRad + 0.2 * startRad * (1 - 2 * getRand()),
angle: getRand() * Math.PI * 2,
gen: 1,
attempts: 0
};
nodes.push(n);
activeNodes.push(n);
addNodeToGrid(n);
}
}
// build a single SVG path string for all lines to keep performance good
let pathData = "";
// 5. Main generation loop
while (nodes.length < maxNodes && activeNodes.length > 0) {
// Pick a random active node to branch from
let activeIdx = Math.floor(getRand() * activeNodes.length);
let kNode = activeNodes[activeIdx];
kNode.attempts++;
if (kNode.attempts > maxAttempts) {
// reached max branching attempts
activeNodes.splice(activeIdx, 1);
continue;
}
let newR = kNode.r * radScale;
if (newR < 1.0) {
// Node dies if it gets too small
kNode.attempts = maxAttempts + 1;
activeNodes.splice(activeIdx, 1);
continue;
}
let newGen = kNode.gen + 1;
let spread = (1.0 - 1.0 / Math.pow(newGen + 1, 0.1));
let newAngle = kNode.angle + spread * getNormal() * rad(searchAngle);
let newX = kNode.x + Math.sin(newAngle) * newR;
let newY = kNode.y + Math.cos(newAngle) * newR;
// Check boundary
if (Math.sqrt(newX*newX + newY*newY) > boundaryRadius) { continue; }
// Check collisions against nearby nodes using spatial hash
let nearby = getNearbyNodes(newX, newY);
let collision = false;
for (let other of nearby) {
if (other === kNode) continue; // Ignore parent (they touch by definition)
let dx = other.x - newX;
let dy = other.y - newY;
let distSq = dx*dx + dy*dy;
let minSpace = other.r + newR;
if (distSq * 2 < minSpace * minSpace) {
collision = true;
break;
}
}
if (!collision) {
let newNode = {
x: newX,
y: newY,
r: newR,
angle: newAngle,
gen: newGen,
attempts: 0
};
nodes.push(newNode);
activeNodes.push(newNode);
addNodeToGrid(newNode);
// Add line connecting parent to child
pathData += "M " + kNode.x.toFixed(2) + " " + kNode.y.toFixed(2) + " L " + newX.toFixed(2) + " " + newY.toFixed(2) + " ";
}
}
// 6. Output generation
// Create the unified branch lines
let branchLines = create.path({ d: pathData }).stroke({ color: color, width: 1, linecap: 'round', join: 'round' }).fill('none');
output.add(branchLines);
// Optionally create the "Sandpaint" style dots at the joints
if (drawDots) {
let dots = [];
nodes.forEach(n => {
let dot = create.ellipse({ radiusX: n.r * 0.35, radiusY: n.r * 0.35 }).translate(n.x, n.y).fill(color).stroke('none');
dots.push(dot);
});
// Add all dots as a unified group so downstream nodes can manipulate them together
output.addGroup('Nodes', dots);
}// 1. UI Controls
let maxNodes = ui.number('Max Nodes', 2000, 100, 5000, 100);
let initSeeds = ui.number('Initial Seeds', 9, 1, 50, 1);
let startRad = ui.number('Start Radius', 20, 5, 100);
let radScale = ui.number('Radius Scale', 0.95, 0.8, 1.0, 0.01);
let searchAngle = ui.number('Search Angle (Deg)', 180, 0, 360);
let maxAttempts = ui.number('Max Attempts', 15, 1, 50, 1);
let boundaryRadius = ui.number('Boundary Radius', 400, 100, 1000);
let seedVal = ui.number('Random Seed', 42, 0, 1000, 1);
let drawDots = ui.toggle('Draw Nodes', true);
let color = ui.color('Color', '#333333');
// 2. Randomness (deterministic)
let rndCounter = 0;
function getRand() {
rndCounter++;
return seededRandom(seedVal + rndCounter);
}
// emulate numpy.random.normal() using Box-Muller transform
function getNormal() {
let u = 1 - getRand();
let v = getRand();
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}
// 3. Spatial hashing grid (optimization for collision detection)
const cellSize = startRad * 2;
const spatialGrid = new Map();
function getGridKey(x, y) {
return Math.floor(x/cellSize) + ',' + Math.floor(y/cellSize);
}
function addNodeToGrid(n) {
let key = getGridKey(n.x, n.y);
if (!spatialGrid.has(key)) spatialGrid.set(key, []);
spatialGrid.get(key).push(n);
}
function getNearbyNodes(x, y) {
let gx = Math.floor(x/cellSize);
let gy = Math.floor(y/cellSize);
let nearby = [];
for(let i = -1; i <= 1; i++) {
for(let j = -1; j <= 1; j++) {
let key = (gx+i) + ',' + (gy+j);
if (spatialGrid.has(key)) nearby.push(...spatialGrid.get(key));
}
}
return nearby;
}
// 4. Initialize seed nodes
let nodes = [];
let activeNodes = []; // Nodes that can still branch
while (nodes.length < initSeeds) {
let x = (getRand() * 2 - 1) * boundaryRadius;
let y = (getRand() * 2 - 1) * boundaryRadius;
// Ensure seeds start inside the boundary circle
if (Math.sqrt(x*x + y*y) < boundaryRadius * 0.9) {
let n = {
x: x,
y: y,
r: startRad + 0.2 * startRad * (1 - 2 * getRand()),
angle: getRand() * Math.PI * 2,
gen: 1,
attempts: 0
};
nodes.push(n);
activeNodes.push(n);
addNodeToGrid(n);
}
}
// build a single SVG path string for all lines to keep performance good
let pathData = "";
// 5. Main generation loop
while (nodes.length < maxNodes && activeNodes.length > 0) {
// Pick a random active node to branch from
let activeIdx = Math.floor(getRand() * activeNodes.length);
let kNode = activeNodes[activeIdx];
kNode.attempts++;
if (kNode.attempts > maxAttempts) {
// reached max branching attempts
activeNodes.splice(activeIdx, 1);
continue;
}
let newR = kNode.r * radScale;
if (newR < 1.0) {
// Node dies if it gets too small
kNode.attempts = maxAttempts + 1;
activeNodes.splice(activeIdx, 1);
continue;
}
let newGen = kNode.gen + 1;
let spread = (1.0 - 1.0 / Math.pow(newGen + 1, 0.1));
let newAngle = kNode.angle + spread * getNormal() * rad(searchAngle);
let newX = kNode.x + Math.sin(newAngle) * newR;
let newY = kNode.y + Math.cos(newAngle) * newR;
// Check boundary
if (Math.sqrt(newX*newX + newY*newY) > boundaryRadius) { continue; }
// Check collisions against nearby nodes using spatial hash
let nearby = getNearbyNodes(newX, newY);
let collision = false;
for (let other of nearby) {
if (other === kNode) continue; // Ignore parent (they touch by definition)
let dx = other.x - newX;
let dy = other.y - newY;
let distSq = dx*dx + dy*dy;
let minSpace = other.r + newR;
if (distSq * 2 < minSpace * minSpace) {
collision = true;
break;
}
}
if (!collision) {
let newNode = {
x: newX,
y: newY,
r: newR,
angle: newAngle,
gen: newGen,
attempts: 0
};
nodes.push(newNode);
activeNodes.push(newNode);
addNodeToGrid(newNode);
// Add line connecting parent to child
pathData += "M " + kNode.x.toFixed(2) + " " + kNode.y.toFixed(2) + " L " + newX.toFixed(2) + " " + newY.toFixed(2) + " ";
}
}
// 6. Output generation
// Create the unified branch lines
let branchLines = create.path({ d: pathData }).stroke({ color: color, width: 1, linecap: 'round', join: 'round' }).fill('none');
output.add(branchLines);
// Optionally create the "Sandpaint" style dots at the joints
if (drawDots) {
let dots = [];
nodes.forEach(n => {
let dot = create.ellipse({ radiusX: n.r * 0.35, radiusY: n.r * 0.35 }).translate(n.x, n.y).fill(color).stroke('none');
dots.push(dot);
});
// Add all dots as a unified group so downstream nodes can manipulate them together
output.addGroup('Nodes', dots);
} Node parameters
| Parameter | Description |
|---|---|
Label | The display name for the node. |
Snippet API reference
UI Generation
ui.number('Label', default?: number, min?: number, max?: number, step?: number)
Dynamically generates a number slider in the node's parameter panel. Returns the current value of the slider.
Example
let radius = ui.number('Radius', 50, 10, 100);ui.color('Label', defaultHex?: string)
Dynamically generates a color picker in the node's parameter panel. Returns the selected HEX string.
Example
let fillCol = ui.color('Shape Color', '#ff0055');ui.toggle('Label', default?: boolean)
Dynamically generates a boolean checkbox toggle. Returns true or false.
Example
if (ui.toggle('Show Outline', false)) { ... }ui.text('Label', default?: string)
Dynamically generates a text input field. Returns the string value.
Example
let myLabel = ui.text('Label', 'Hello World');Graph Data
inputs.shapes: ShapeWrapper[]
An array of all geometric shapes connected to the node's input. Modifying these automatically updates the output unless you explicitly call output.add().
Example
inputs.shapes.forEach(s => s.translate(10, 0));inputs.points: PointAttributes[]
An array of all point cloud data (DNA) connected to the node's input. Modify point positions or attributes in-place.
Example
inputs.points.forEach(pt => pt.position.y += 10);output.add(...items: (ShapeWrapper | EvaluatedObject)[])
Outputs generated or modified shapes. Calling this disables auto-passthrough of input shapes. You can pass a single shape (e.g. output.add(rect)) or multiple items.
Example
output.add(myNewRect);output.addGroup(name: string, items: (ShapeWrapper | EvaluatedObject)[])
Groups specified shapes into a logical group that can be targeted by downstream nodes (e.g., Transform).
Example
output.addGroup('My Group', [rect1, rect2]);Geometry Creation
node(type: string, properties: Record<string, any>, primaryInputs?: ShapeWrapper[], secondaryInputs?: ShapeWrapper[], tertiaryInputs?: ShapeWrapper[]): ShapeWrapper[]
Runs any built-in node evaluator natively. It processes provided input shape arrays using the properties, exactly as if connected in the nodegraph.
Example
let trimmed = node('trimPath', { start: 0.1, end: 0.9, preservePoints: true }, [myPath]);create.rectangle(properties: { width?: number, height?: number, cornerRadius?: {tl, tr, bl, br} })
Instantiates a new rectangle.
Example
let box = create.rectangle({ width: 100, height: 100 });create.ellipse(properties: { radiusX?: number, radiusY?: number })
Instantiates a new ellipse.
Example
let circle = create.ellipse({ radiusX: 50, radiusY: 50 });create.star(properties: { points?: number, outerRadius?: number, innerRadius?: number })
Instantiates a new star shape.
Example
let s = create.star({ points: 5, outerRadius: 50, innerRadius: 25 });create.polygon(properties: { sides?: number, radius?: number })
Instantiates a new polygon.
Example
let p = create.polygon({ sides: 6, radius: 50 });create.arrow(properties: { shaftLength?: number, shaftWidth?: number, headLength?: number, headWidth?: number })
Instantiates a new arrow shape.
Example
let arr = create.arrow({ shaftLength: 100, shaftWidth: 20 });create.donut(properties: { outerRadius?: number, innerRadius?: number })
Instantiates a new donut shape.
Example
let d = create.donut({ outerRadius: 50, innerRadius: 30 });create.trapezoid(properties: { topWidth?: number, bottomWidth?: number, height?: number })
Instantiates a new trapezoid shape.
Example
let t = create.trapezoid({ topWidth: 40, bottomWidth: 80, height: 60 });create.spiral(properties: { startRadius?: number, endRadius?: number, revolutions?: number, points?: number, spiralType?: string })
Instantiates a new spiral shape.
Example
let s = create.spiral({ revolutions: 3, endRadius: 100 });create.path(properties: { d?: string })
Instantiates a new SVG path.
Example
let p = create.path({ d: 'M0,0 L100,100' });create.text(properties: { content?: string, fontSize?: number, fontFamily?: string, letterSpacing?: number, lineHeight?: number })
Instantiates a new text shape.
Example
let t = create.text({ content: 'Hello', fontSize: 48 });shape.translate(x: number, y: number)
A chainable method that moves a shape by X and Y coordinates.
Example
myShape.translate(50, 0).rotate(45);shape.rotate(angleDegrees: number)
A chainable method that rotates a shape by the specified degrees.
Example
myShape.rotate(time * 90);shape.scale(x: number, y: number)
A chainable method that scales a shape uniformly or non-uniformly.
Example
myShape.scale(1.5, 1.5);shape.opacity(value: number)
A chainable method that sets the opacity of the shape (0 to 1).
Example
myShape.opacity(0.5);shape.blur(amount: number)
A chainable method that applies a Gaussian blur to the shape.
Example
myShape.blur(10);shape.blendMode(mode: 'normal' | 'multiply' | 'screen' | 'overlay' | 'darken' | 'lighten' | 'color-dodge' | 'color-burn' | 'hard-light' | 'soft-light' | 'difference' | 'exclusion' | 'hue' | 'saturation' | 'color' | 'luminosity')
A chainable method that sets the CSS blend mode of the shape.
Example
myShape.blendMode('multiply');shape.fill(options: string | { color?: string, opacity?: number, blendMode?: string })
A chainable method that adds or updates the primary fill layer of a shape. Accepts a HEX color string or an options object.
Example
myShape.fill({ color: '#ff0000', opacity: 0.5 });shape.stroke(options: string | { color?: string, width?: number, opacity?: number, dash?: number[], dashSpacing?: number, join?: string, linecap?: string, align?: string }, width?: number)
A chainable method that adds or updates the primary stroke layer of a shape.
Example
myShape.stroke({ color: '#ffffff', width: 2, dash: [4, 4], align: 'inside' });shape.shadow(options: { color?: string, blur?: number, offset?: {x: number, y: number}, opacity?: number })
A chainable method that adds a drop shadow to the shape.
Example
myShape.shadow({ color: '#000000', blur: 10, offset: {x: 5, y: 5} });shape.innerShadow(options: { color?: string, blur?: number, offset?: {x: number, y: number}, opacity?: number })
A chainable method that adds an inner shadow to the shape. Same options as shadow().
Example
myShape.innerShadow({ color: '#ffffff', blur: 5 });Context Variables
time: number
The current global animation time in seconds.
Example
let wave = Math.sin(time);frame: number
The current global animation frame number.
Example
if (frame === 10) { ... }Math
noise(x: number, y: number, seed?: number): number
Generates 2D Simplex noise. Returns a value between -1 and 1.
Example
let n = noise(px * 0.1, py * 0.1, 42);deg(radians: number): number
Converts radians to degrees.
Example
let d = deg(Math.PI); // 180rad(degrees: number): number
Converts degrees to radians.
Example
let r = rad(180); // 3.14159distance(p1: Point, p2: Point): number
Calculates the Euclidean distance between two points.
Example
let d = distance({x: 0, y: 0}, {x: 100, y: 100});lerp(t: number, p1: Point, p2: Point): Point
Linearly interpolates between two points.
Example
let mid = lerp(0.5, ptA, ptB);lerpNumber(a: number, b: number, t: number): number
Linearly interpolates between two numbers.
Example
let val = lerpNumber(0, 100, 0.5); // 50map(v: number, inMin: number, inMax: number, outMin: number, outMax: number): number
Maps a value from a source range to a target range.
Example
let scaled = map(0.5, 0, 1, 0, 100); // 50seededRandom(seed: number): number
Returns a deterministic pseudo-random number between 0 and 1 based on the seed.
Example
let r = seededRandom(42);