AnimGraphLab Beta
Nodes Snippet

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 });

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);

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);

Output

Finally, output the shape so it can be rendered or processed by downstream nodes.

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);

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);

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);
});

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);

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);
}

Node parameters

ParameterDescription
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); // 180

rad(degrees: number): number

Converts degrees to radians.

Example

let r = rad(180); // 3.14159

distance(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); // 50

map(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); // 50

seededRandom(seed: number): number

Returns a deterministic pseudo-random number between 0 and 1 based on the seed.

Example

let r = seededRandom(42);

See also