Skip to content

Custom Rendering

While Blobit nodes look great by default, sometimes you need to visualize data directly on the node itself—whether it’s an image preview, a color ramp, or a custom status indicator.

You can customize the appearance of your node by overriding two specific methods in your node class: onDrawBackground and onDrawForeground.

Drawing on a node assumes a standard 2D Cartesian coordinate system:

  • 0,0 (Origin): The top-left corner of the node’s body (just below the title bar).
  • Width: this.size[0]
  • Height: this.size[1]

Best for: Static Content & Heavy Visuals Draws behind all widgets, input/output points, and text.

onDrawBackground(ctx) {
// Check flags first!
if (this.flags.collapsed) return;
// Draw a red background
ctx.fillStyle = "red";
ctx.fillRect(0, 0, this.size[0], this.size[1]);
}

Best for: UI Overlays & Status Indicators Draws on top of everything, including widgets.

onDrawForeground(ctx) {
if (this.flags.collapsed) return;
// Draw a blue circle overlay
ctx.fillStyle = "blue";
ctx.beginPath();
ctx.arc(10, 10, 5, 0, Math.PI*2);
ctx.fill();
}

Crucial for Animation Blobit tries to be efficient and only redraws the graph when necessary. If your custom rendering changes (e.g., an image loads, or an animation frame advances), you main need to force a redraw.

  • fg (boolean): Redraw the foreground (widgets, overlays).
  • bg (boolean): Redraw the background (wires, grid).
// Example: Force a redraw after an image finishes loading
this._image.onload = () => {
this.setDirtyCanvas(true, true);
};

This example draws a small “LED” light in the top-right corner that changes color based on a property. It’s a great way to show if a node is “Active” or “Bypassed” without cluttering the UI.

onDrawForeground(ctx) {
// Optimization: Don't draw if the node is minimized!
if (this.flags.collapsed) return;
// 1. Determine State
const isRunning = this.properties.status === "running";
// 2. Setup Style
ctx.fillStyle = isRunning ? "#00FF00" : "#330000";
// 3. Draw Circle (Top-Right)
const x = this.size[0] - 20;
const y = 20;
const radius = 6;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
// 4. Add Glow (Optional)
if (isRunning) {
ctx.shadowColor = "#00FF00";
ctx.shadowBlur = 10;
ctx.stroke();
ctx.shadowBlur = 0; // Reset state!
}
}

2. Image Preview with correct Aspect Ratio (Background)

Section titled “2. Image Preview with correct Aspect Ratio (Background)”

Drawing an image is tricky because you don’t want to stretch it. This snippet calculates the correct aspect ratio to “fit” the image inside the node, adding black bars if necessary.

onDrawBackground(ctx) {
if (this.flags.collapsed) return;
// 1. Setup Drawing Area (exclude padding)
const padding = 10;
const drawW = this.size[0] - padding * 2;
const drawH = this.size[1] - padding * 2;
// Draw a dark background container first
ctx.fillStyle = "#111";
ctx.fillRect(padding, padding, drawW, drawH);
// 2. Check if we have a valid image to draw
if (this._image && this._image.complete && this._image.naturalWidth > 0) {
// 3. Calculate Scale Calculation (Object Fit: Contain)
const imgW = this._image.naturalWidth;
const imgH = this._image.naturalHeight;
const aspect = imgW / imgH;
let targetW = drawW;
let targetH = drawH;
// If image is wider relative to container -> Constrain Width
if (targetW / targetH > aspect) {
targetW = targetH * aspect;
}
// If image is taller relative to container -> Constrain Height
else {
targetH = targetW / aspect;
}
// 4. Center the Image
const x = padding + (drawW - targetW) / 2;
const y = padding + (drawH - targetH) / 2;
// 5. Draw
// syntax: drawImage(img, x, y, width, height)
ctx.drawImage(this._image, x, y, targetW, targetH);
// Optional: Draw Resolution Text
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(x, y + targetH - 20, targetW, 20);
ctx.fillStyle = "#FFF";
ctx.font = "10px Arial";
ctx.fillText(`${imgW}x${imgH}`, x + 5, y + targetH - 5);
}
}

You can make your custom visuals clickable by handling mouse events. This is how nodes like ColorRamp or CurveEditor work.

Requirement: You must handle onMouseDown and return true if your element was clicked, otherwise logic falls through to standard node dragging.

// Step 1: Draw the button
onDrawForeground(ctx) {
if (this.flags.collapsed) return;
// Change color if "pressed"
ctx.fillStyle = this._buttonPressed ? "#666" : "#444";
ctx.fillRect(10, 10, 100, 30); // x, y, w, h
ctx.fillStyle = "#FFF";
ctx.font = "12px Arial";
ctx.fillText("Click Me", 35, 30);
}
// Step 2: Handle Mouse Down
onMouseDown(event, pos) {
const [x, y] = pos;
// Simple Hit Test: Is the mouse inside our rectangle?
if (x >= 10 && x <= 110 && y >= 10 && y <= 40) {
this._buttonPressed = true;
this.setDirtyCanvas(true, false); // Force Redraw to show "pressed" state
console.log("Custom Action Triggered!");
return true; // STOP event propagation (don't drag node)
}
// Crucial: Call super to allow standard behaviors (like selecting the node) if we didn't click the button
return super.onMouseDown(event, pos);
}
// Step 3: Handle Mouse Up (Cleanup)
onMouseUp(event, pos) {
if (this._buttonPressed) {
this._buttonPressed = false;
this.setDirtyCanvas(true, false); // Force Redraw to reset color
}
return super.onMouseUp(event, pos);
}