Agent skill
Excalidraw Generation
This skill should be used when the user asks to "create excalidraw diagram", "generate excalidraw", "hand-drawn diagram", "sketch diagram", "whiteboard style diagram", or when informal, spatial, annotated diagrams would best convey conceptual relationships. Expert in both presentation design AND artistic Excalidraw JSON creation.
Install this agent skill to your Project
npx add-skill https://github.com/rhuss/cc-slidev/tree/main/slidev/skills/excalidraw-generation
SKILL.md
Excalidraw Generation Expert
Core Philosophy: Semantic redesign, not mechanical conversion. Think like both a presentation designer (clarity, accessibility, simplicity) and an artist (creative visual expression, spatial design, aesthetic beauty).
CRITICAL - Rendering Rule
ALWAYS use render-excalidraw.sh for SVG conversion - NO EXCEPTIONS
After creating Excalidraw JSON:
- Save JSON to
diagrams/<slug>.excalidraw - MUST render using:
${CLAUDE_PLUGIN_ROOT}/scripts/render-excalidraw.sh - NEVER attempt manual SVG conversion
- NEVER embed JSON in markdown - only reference the rendered SVG
The script handles all rendering automatically with excalidraw-brute-export-cli.
When to Use This Skill
Auto-trigger when:
- User explicitly requests: "create excalidraw diagram", "hand-drawn diagram", "sketch", "whiteboard"
- Slide content suggests conceptual/spatial relationships
- Architecture with nested components
- Brainstorming/ideation context
- Informal, approachable style needed
- Annotations and callouts would add value
Diagram type suitability:
- ✅✅✅ BEST: Conceptual relationships, architecture diagrams, mind maps, timelines, comparisons
- ✅✅ EXCELLENT: Flowcharts (with annotations), spatial layouts, nested structures
- ⚠️ OKAY: Sequence diagrams (prefer Mermaid instead)
- ❌ NOT RECOMMENDED: Formal UML (use PlantUML), state machines (use Mermaid)
Evidence-Based Design Constraints (HARD LIMITS)
These constraints are NON-NEGOTIABLE. Enforce strictly:
- Cognitive load: Maximum 9 elements (7±2 rule from cognitive psychology)
- Accessibility:
- Colorblind-safe palette ONLY: Blue #3b82f6 + Orange #f97316
- Minimum 4.5:1 contrast ratio for all text (WCAG AA)
- Never rely on color alone to convey information
- Minimal text: Under 50 words total per diagram
- One idea per diagram: If concept is complex, split into multiple diagrams
- Hand-drawn aesthetic: Roughness 1 for informal feel
Core Capabilities
1. Semantic Concept Extraction
Process (ALWAYS follow this order):
-
Analyze user's description or slide content
-
Extract core concepts (entities, relationships, flows)
-
Identify semantic type:
- Containment: X contains Y → Use nested boxes/frames
- Flow: A→B→C → Use arrows with spatial progression
- Comparison: X vs Y → Use side-by-side separation
- Hierarchy: Parent-child → Use vertical/spatial positioning
- Grouping: Related items → Use frames or color-coded regions
- Annotation: Context/explanation → Use callouts and bound text
-
Design layout (choose from layout algorithms below)
-
Generate JSON (use element factories below)
Example:
Input: "Kubernetes device plugin architecture"
Semantic analysis:
- Type: Architecture + Flow
- Key concepts: Control Plane (container), Worker Node (container),
GPU (component), Device Plugin (component), Kubelet (component)
- Relationships: Discovery flow, Registration flow, Capacity updates
- Spatial meaning: Control Plane ABOVE Worker Node (hierarchy)
Design choice: Vertical layout with 2 frames, 5 shapes, 3 arrows, 3 annotations
Cognitive load: 2 frames + 5 shapes = 7 units ✓
2. Element Factories
These functions generate valid Excalidraw JSON elements. Use them to build diagrams.
ID Generation
function generateId() {
// Excalidraw uses random alphanumeric IDs (12+ chars)
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
Rectangle Factory
function createRectangle(x, y, width, height, text = null, options = {}) {
const id = generateId();
const element = {
type: "rectangle",
version: 1,
versionNonce: Math.floor(Math.random() * 1000000),
isDeleted: false,
id: id,
fillStyle: options.fillStyle || "hachure",
strokeWidth: options.strokeWidth || 2,
strokeStyle: "solid",
roughness: options.roughness !== undefined ? options.roughness : 1,
opacity: 100,
angle: options.angle || 0,
x: x,
y: y,
strokeColor: options.strokeColor || THEME_COLORS.primary,
backgroundColor: options.backgroundColor || "transparent",
width: width,
height: height,
seed: Math.floor(Math.random() * 1000000),
groupIds: options.groupIds || [],
frameId: options.frameId || null,
roundness: { type: 3 },
boundElements: [],
updated: Date.now(),
link: null,
locked: false
};
// If text provided, create bound text element
if (text) {
const textElement = createBoundText(text, id, x, y, width, height);
element.boundElements.push({ type: "text", id: textElement.id });
return [element, textElement]; // Return array
}
return element; // Return single element
}
Text Factory (Standalone and Bound)
function createText(text, x, y, options = {}) {
return {
type: "text",
version: 1,
versionNonce: Math.floor(Math.random() * 1000000),
isDeleted: false,
id: options.id || generateId(),
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 0, // Text is always smooth
opacity: 100,
angle: 0,
x: x,
y: y,
strokeColor: options.strokeColor || THEME_COLORS.text,
backgroundColor: "transparent",
width: options.width || 200,
height: options.height || 25,
seed: Math.floor(Math.random() * 1000000),
groupIds: options.groupIds || [],
frameId: options.frameId || null,
roundness: null,
boundElements: [],
updated: Date.now(),
link: null,
locked: false,
fontSize: options.fontSize || 20,
fontFamily: 1, // 1 = Excalifont/Virgil (hand-drawn), 2 = Helvetica, 3 = Cascadia
text: text,
textAlign: options.textAlign || "center",
verticalAlign: options.verticalAlign || "middle",
containerId: options.containerId || null,
originalText: text,
lineHeight: 1.25,
baseline: 18
};
}
// Font family mapping for reference:
// fontFamily: 1 → Excalifont/Virgil (hand-drawn, default for Excalidraw aesthetic)
// fontFamily: 2 → Helvetica (clean, modern)
// fontFamily: 3 → Cascadia (monospace, code)
// When rendering to SVG: font-family: 'Excalifont', 'Virgil', cursive, sans-serif
function createBoundText(text, containerId, containerX, containerY, containerWidth, containerHeight) {
// Calculate centered position inside container
const textWidth = Math.min(containerWidth - 20, 200);
const textHeight = 25;
const textX = containerX + (containerWidth - textWidth) / 2;
const textY = containerY + (containerHeight - textHeight) / 2;
return createText(text, textX, textY, {
width: textWidth,
height: textHeight,
containerId: containerId,
textAlign: "center",
verticalAlign: "middle"
});
}
Arrow Factory
function createArrow(startX, startY, endX, endY, options = {}) {
const points = [
[0, 0], // Start point (relative to x, y)
[endX - startX, endY - startY] // End point (relative)
];
return {
type: "arrow",
version: 1,
versionNonce: Math.floor(Math.random() * 1000000),
isDeleted: false,
id: generateId(),
fillStyle: "hachure",
strokeWidth: options.strokeWidth || 2,
strokeStyle: "solid",
roughness: options.roughness !== undefined ? options.roughness : 1,
opacity: 100,
angle: 0,
x: startX,
y: startY,
strokeColor: options.strokeColor || THEME_COLORS.neutral,
backgroundColor: "transparent",
width: Math.abs(endX - startX),
height: Math.abs(endY - startY),
seed: Math.floor(Math.random() * 1000000),
groupIds: options.groupIds || [],
frameId: options.frameId || null,
roundness: { type: 2 },
boundElements: [],
updated: Date.now(),
link: null,
locked: false,
startBinding: options.startBinding || null,
endBinding: options.endBinding || null,
lastCommittedPoint: null,
startArrowhead: null,
endArrowhead: "arrow",
points: points
};
}
Frame Factory (Containers)
function createFrame(x, y, width, height, name, options = {}) {
return {
type: "frame",
version: 1,
versionNonce: Math.floor(Math.random() * 1000000),
isDeleted: false,
id: generateId(),
fillStyle: "hachure",
strokeWidth: 2,
strokeStyle: "solid",
roughness: 0, // Frames are clean, not hand-drawn
opacity: 100,
angle: 0,
x: x,
y: y,
strokeColor: options.strokeColor || THEME_COLORS.neutral,
backgroundColor: options.backgroundColor || THEME_COLORS.light_bg,
width: width,
height: height,
seed: Math.floor(Math.random() * 1000000),
groupIds: [],
frameId: null,
roundness: null,
boundElements: [],
updated: Date.now(),
link: null,
locked: false,
name: name
};
}
Ellipse Factory
function createEllipse(x, y, width, height, text = null, options = {}) {
const id = generateId();
const element = {
type: "ellipse",
version: 1,
versionNonce: Math.floor(Math.random() * 1000000),
isDeleted: false,
id: id,
fillStyle: options.fillStyle || "hachure",
strokeWidth: options.strokeWidth || 2,
strokeStyle: "solid",
roughness: options.roughness !== undefined ? options.roughness : 1,
opacity: 100,
angle: 0,
x: x,
y: y,
strokeColor: options.strokeColor || THEME_COLORS.primary,
backgroundColor: options.backgroundColor || "transparent",
width: width,
height: height,
seed: Math.floor(Math.random() * 1000000),
groupIds: options.groupIds || [],
frameId: options.frameId || null,
roundness: null,
boundElements: [],
updated: Date.now(),
link: null,
locked: false
};
if (text) {
const textElement = createBoundText(text, id, x, y, width, height);
element.boundElements.push({ type: "text", id: textElement.id });
return [element, textElement];
}
return element;
}
Callout Factory (Annotation)
function createCallout(targetX, targetY, text, direction = "top-right") {
const offsets = {
"top-right": { dx: 100, dy: -80 },
"top-left": { dx: -100, dy: -80 },
"bottom-right": { dx: 100, dy: 80 },
"bottom-left": { dx: -100, dy: 80 }
};
const offset = offsets[direction];
const textX = targetX + offset.dx;
const textY = targetY + offset.dy;
const calloutText = createText(text, textX, textY, {
fontSize: 16,
strokeColor: THEME_COLORS.accent
});
const arrow = createArrow(textX, textY + 12, targetX, targetY, {
strokeColor: THEME_COLORS.accent,
strokeWidth: 1.5
});
// Group them together
const groupId = generateId();
calloutText.groupIds.push(groupId);
arrow.groupIds.push(groupId);
return [calloutText, arrow];
}
3. Layout Algorithms
Layout Constants
const LAYOUT = {
MARGIN: 50, // Canvas edge margin
PADDING: 40, // Between elements
NODE_WIDTH: 180, // Standard node width
NODE_HEIGHT: 80, // Standard node height
ARROW_GAP: 10 // Gap for arrow binding
};
Horizontal Flow Layout
function layoutHorizontalFlow(nodes) {
// Left-to-right progression
const positions = [];
let currentX = LAYOUT.MARGIN;
const baseY = 200; // Vertical center
nodes.forEach((node, index) => {
positions.push({
x: currentX,
y: baseY,
width: LAYOUT.NODE_WIDTH,
height: LAYOUT.NODE_HEIGHT,
text: node
});
currentX += LAYOUT.NODE_WIDTH + LAYOUT.PADDING;
});
return positions;
}
Vertical Flow Layout
function layoutVerticalFlow(nodes) {
// Top-to-bottom progression
const positions = [];
const baseX = 300; // Horizontal center
let currentY = LAYOUT.MARGIN;
nodes.forEach((node, index) => {
positions.push({
x: baseX,
y: currentY,
width: LAYOUT.NODE_WIDTH,
height: LAYOUT.NODE_HEIGHT,
text: node
});
currentY += LAYOUT.NODE_HEIGHT + LAYOUT.PADDING;
});
return positions;
}
Radial Layout (Mind Map)
function layoutRadial(centerNode, childNodes) {
const positions = [];
const centerX = 400;
const centerY = 300;
const radius = 200;
// Center node
positions.push({
x: centerX - LAYOUT.NODE_WIDTH / 2,
y: centerY - LAYOUT.NODE_HEIGHT / 2,
width: LAYOUT.NODE_WIDTH,
height: LAYOUT.NODE_HEIGHT,
text: centerNode
});
// Child nodes in circle
const angleStep = (2 * Math.PI) / childNodes.length;
childNodes.forEach((node, index) => {
const angle = index * angleStep;
const x = centerX + radius * Math.cos(angle) - LAYOUT.NODE_WIDTH / 2;
const y = centerY + radius * Math.sin(angle) - LAYOUT.NODE_HEIGHT / 2;
positions.push({
x: x,
y: y,
width: LAYOUT.NODE_WIDTH,
height: LAYOUT.NODE_HEIGHT,
text: node
});
});
return positions;
}
4. Connection Binding Logic
Create Binding Point
function createBindingPoint(shapeId, shapeX, shapeY, shapeWidth, shapeHeight, side) {
// side: "top", "bottom", "left", "right"
let focus = { x: 0, y: 0 };
switch(side) {
case "right":
focus = { x: 1, y: 0 }; // Right edge, centered
break;
case "left":
focus = { x: -1, y: 0 }; // Left edge, centered
break;
case "bottom":
focus = { x: 0, y: 1 }; // Bottom edge, centered
break;
case "top":
focus = { x: 0, y: -1 }; // Top edge, centered
break;
}
return {
elementId: shapeId,
focus: focus,
gap: LAYOUT.ARROW_GAP
};
}
Connect Shapes Horizontally
function connectShapesHorizontal(shapeA, shapeB) {
// Bind arrow from right edge of A to left edge of B
const startX = shapeA.x + shapeA.width;
const startY = shapeA.y + shapeA.height / 2;
const endX = shapeB.x;
const endY = shapeB.y + shapeB.height / 2;
return createArrow(startX, startY, endX, endY, {
startBinding: createBindingPoint(shapeA.id, shapeA.x, shapeA.y, shapeA.width, shapeA.height, "right"),
endBinding: createBindingPoint(shapeB.id, shapeB.x, shapeB.y, shapeB.width, shapeB.height, "left")
});
}
5. Color Palette (Colorblind-Safe)
const THEME_COLORS = {
primary: "#3b82f6", // Blue (8.6:1 contrast) - Main shapes
secondary: "#f97316", // Orange (3.4:1, ≥24pt only) - Emphasis
neutral: "#6b7280", // Gray - Arrows, frames
text: "#1f2937", // Dark gray (16.1:1 contrast) - ALL text
background: "#ffffff", // White canvas
accent: "#8b5cf6", // Purple - Annotations
light_bg: "#f3f4f6" // Light gray - Frame fills
};
Color Usage Rules:
- Primary shapes: Blue stroke, transparent or light fill
- Emphasis shapes: Orange stroke (use sparingly)
- Containers/frames: Gray stroke, light gray fill
- Arrows: Gray (neutral, never distracting)
- All text: Dark gray #1f2937 (maximum readability)
- Annotations: Purple for visual distinction
6. Validation Functions
JSON Structure Validation
function validateExcalidrawJSON(json) {
const errors = [];
// Check required top-level fields
if (json.type !== "excalidraw") {
errors.push("Missing or invalid 'type' (must be 'excalidraw')");
}
if (json.version !== 2) {
errors.push("Version should be 2");
}
if (!Array.isArray(json.elements)) {
errors.push("'elements' must be array");
}
if (typeof json.appState !== "object") {
errors.push("'appState' must be object");
}
// Validate each element
json.elements.forEach((element, index) => {
if (!element.id) errors.push(`Element ${index} missing 'id'`);
if (!element.type) errors.push(`Element ${index} missing 'type'`);
if (typeof element.x !== "number") errors.push(`Element ${index} missing 'x'`);
if (typeof element.y !== "number") errors.push(`Element ${index} missing 'y'`);
// Check bound text references
if (element.boundElements) {
element.boundElements.forEach(bound => {
if (bound.type === "text") {
const textElement = json.elements.find(e => e.id === bound.id);
if (!textElement) {
errors.push(`Bound text ${bound.id} not found`);
}
if (textElement && textElement.containerId !== element.id) {
errors.push(`Bound text ${bound.id} containerId mismatch`);
}
}
});
}
// Check arrow bindings
if (element.type === "arrow") {
if (element.startBinding && element.startBinding.elementId) {
const target = json.elements.find(e => e.id === element.startBinding.elementId);
if (!target) {
errors.push(`Arrow ${element.id} startBinding target not found`);
}
}
if (element.endBinding && element.endBinding.elementId) {
const target = json.elements.find(e => e.id === element.endBinding.elementId);
if (!target) {
errors.push(`Arrow ${element.id} endBinding target not found`);
}
}
}
});
return {
valid: errors.length === 0,
errors: errors
};
}
Cognitive Load Validation
function countCognitiveElements(json) {
// Count distinct visual concepts (not total elements)
const cognitiveUnits = {
shapes: 0,
arrows: 0,
annotations: 0,
frames: 0
};
const groupedElements = new Set();
json.elements.forEach(element => {
// Skip if part of counted group
if (element.groupIds && element.groupIds.length > 0) {
if (groupedElements.has(element.groupIds[0])) {
return; // Already counted
}
groupedElements.add(element.groupIds[0]);
}
// Skip bound text (counted with parent)
if (element.containerId) return;
switch(element.type) {
case "rectangle":
case "ellipse":
case "diamond":
cognitiveUnits.shapes++;
break;
case "arrow":
case "line":
cognitiveUnits.arrows++;
break;
case "text":
cognitiveUnits.annotations++;
break;
case "frame":
cognitiveUnits.frames++;
break;
}
});
const total = cognitiveUnits.shapes +
cognitiveUnits.arrows +
cognitiveUnits.annotations +
cognitiveUnits.frames;
return {
breakdown: cognitiveUnits,
total: total,
withinLimit: total <= 9, // 7±2 rule
recommendation: total > 9 ? "SPLIT into multiple diagrams" : "Good"
};
}
7. Assembly Function
function assembleExcalidrawJSON(elements) {
return {
type: "excalidraw",
version: 2,
source: "https://excalidraw.com",
elements: elements,
appState: {
viewBackgroundColor: THEME_COLORS.background,
gridSize: null,
theme: "light"
},
files: {}
};
}
Interactive Workflow
Follow this workflow when generating diagrams:
Step 1: Analyze Concept
Ask the user (if not clear from context):
- What's the main concept to convey?
- Who's the audience? (beginners, experts, mixed)
- Should this be formal or informal?
- Are there specific relationships/flows to highlight?
Step 2: Design Proposal
Show the user your design approach:
## Diagram Analysis
Semantic type: [Architecture/Flow/Mind Map/etc.]
Best platform: Excalidraw
Design approach:
- Layout: [Horizontal/Vertical/Radial]
- Elements: [List main shapes and their purpose]
- Frames: [If using containers]
- Arrows: [Key flows to show]
- Annotations: [Callouts for context]
- Hand-drawn aesthetic: roughness 1
- Colors: Colorblind-safe blue/orange
Element count: [N] shapes + [M] arrows + [P] annotations = [Total]
Grouped into [X] logical units → within cognitive limit ✓ / ⚠️ OVER LIMIT
Step 3: Show ASCII Preview
Show a text-based preview:
┌─────────────────────────────────┐
│ Container Name │
│ ┌──────────┐ ┌──────────┐ │
│ │ Shape A │───→│ Shape B │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────┘
↑ "Annotation explaining flow"
Ask: "Proceed with JSON generation?"
Step 4: Generate JSON
- Create frames (if needed)
- Create shapes with bound text
- Position using layout algorithm
- Create arrows with bindings
- Add annotation callouts
- Apply grouping (if needed)
- Assemble final JSON
- Validate structure
- Check cognitive load
- Save to file
Step 5: Save and Render
File structure:
diagrams/
└── <slide-title-slug>.excalidraw # JSON source (editable)
public/images/<slide-title-slug>/
└── diagram-excalidraw.svg # Rendered SVG (for slide)
IMPORTANT: Always save source files to ./diagrams/ directory.
CRITICAL - Rendering Process:
-
Save JSON: Use Write tool to save JSON to
diagrams/<slug>.excalidraw -
Render to SVG: ALWAYS use
render-excalidraw.shscript - NEVER attempt manual rendering:bash${CLAUDE_PLUGIN_ROOT}/scripts/render-excalidraw.sh \ diagrams/<slug>.excalidraw \ public/images/<slug>/diagram-excalidraw.svg -
Script handles: The script automatically:
- Installs excalidraw-brute-export-cli if missing
- Installs playwright chromium dependencies
- Renders with correct parameters (--background 1, --embed-scene 0, etc.)
- Ensures proper font rendering (Excalifont → Virgil → cursive → sans-serif)
DO NOT attempt to render Excalidraw any other way. ALWAYS use the script.
Step 6: Offer Iterations
✅ Excalidraw Diagram Generated!
Source: diagrams/<slug>.excalidraw
Rendered: public/images/<slug>/diagram-excalidraw.svg
Edit online: https://excalidraw.com (drag diagrams/<slug>.excalidraw file)
After editing, re-render with:
${CLAUDE_PLUGIN_ROOT}/scripts/render-excalidraw.sh \
diagrams/<slug>.excalidraw \
public/images/<slug>/diagram-excalidraw.svg
Refinement options:
- Adjust layout (horizontal ↔ vertical)
- Add more annotations
- Change colors/emphasis
- Simplify (remove elements)
- Add more detail
What would you like to adjust?
Example Generation: Architecture Diagram
Input: "Show Kubernetes device plugin architecture"
Step 1: Semantic Analysis
Type: Architecture + Flow
Concepts:
- Control Plane (container) - top
- Worker Node (container) - bottom
- Inside Worker Node: GPU, Device Plugin, Kubelet
- Flow: Discovery → Registration → Capacity Updates
Layout: Vertical (hierarchy)
Element count: 2 frames + 5 shapes + 3 arrows + 2 annotations = 12 base
BUT: Grouped into 2 logical units (control plane, worker node) = 7 cognitive units ✓
Step 2: JSON Generation
const elements = [];
// Create frames
const controlPlaneFrame = createFrame(50, 50, 700, 200, "Control Plane");
const workerNodeFrame = createFrame(50, 300, 700, 300, "Worker Node");
elements.push(controlPlaneFrame, workerNodeFrame);
// Control plane components
const [scheduler, schedulerText] = createRectangle(100, 100, 180, 80, "Scheduler", {
frameId: controlPlaneFrame.id,
strokeColor: THEME_COLORS.primary
});
const [apiServer, apiServerText] = createRectangle(400, 100, 180, 80, "API Server", {
frameId: controlPlaneFrame.id,
strokeColor: THEME_COLORS.primary
});
elements.push(scheduler, schedulerText, apiServer, apiServerText);
// Worker node components
const [gpu, gpuText] = createRectangle(100, 350, 180, 80, "GPU 0\nGPU 1", {
frameId: workerNodeFrame.id,
strokeColor: THEME_COLORS.secondary // Orange for emphasis
});
const [plugin, pluginText] = createRectangle(350, 350, 180, 80, "Device Plugin", {
frameId: workerNodeFrame.id,
strokeColor: THEME_COLORS.primary
});
const [kubelet, kubeletText] = createRectangle(350, 480, 180, 80, "Kubelet", {
frameId: workerNodeFrame.id,
strokeColor: THEME_COLORS.primary
});
elements.push(gpu, gpuText, plugin, pluginText, kubelet, kubeletText);
// Arrows with bindings
const arrow1 = connectShapesHorizontal(gpu, plugin); // Discovery
const arrow2 = createArrow(
plugin.x + plugin.width / 2, plugin.y + plugin.height,
kubelet.x + kubelet.width / 2, kubelet.y,
{
startBinding: createBindingPoint(plugin.id, plugin.x, plugin.y, plugin.width, plugin.height, "bottom"),
endBinding: createBindingPoint(kubelet.id, kubelet.x, kubelet.y, kubelet.width, kubelet.height, "top")
}
);
const arrow3 = createArrow(
kubelet.x + kubelet.width / 2, kubelet.y,
apiServer.x + apiServer.width / 2, apiServer.y + apiServer.height,
{
startBinding: createBindingPoint(kubelet.id, kubelet.x, kubelet.y, kubelet.width, kubelet.height, "top"),
endBinding: createBindingPoint(apiServer.id, apiServer.x, apiServer.y, apiServer.width, apiServer.height, "bottom")
}
);
elements.push(arrow1, arrow2, arrow3);
// Annotations
const [annotation1Text, annotation1Arrow] = createCallout(
plugin.x + plugin.width, plugin.y + plugin.height / 2,
"Your code",
"top-right"
);
const [annotation2Text, annotation2Arrow] = createCallout(
scheduler.x + scheduler.width / 2, scheduler.y,
"Now aware!",
"top-left"
);
elements.push(annotation1Text, annotation1Arrow, annotation2Text, annotation2Arrow);
// Assemble and validate
const json = assembleExcalidrawJSON(elements);
const validation = validateExcalidrawJSON(json);
const cognitiveCheck = countCognitiveElements(json);
if (!validation.valid) {
console.error("Validation errors:", validation.errors);
// Fix or abort
}
if (!cognitiveCheck.withinLimit) {
console.warn(`Cognitive overload: ${cognitiveCheck.total} elements`);
// Suggest splitting
}
// Save to file (source goes in diagrams/)
const filePath = "diagrams/device-plugin-architecture.excalidraw";
writeFile(filePath, JSON.stringify(json, null, 2));
Diagram Type Patterns
Flowchart Pattern
// Horizontal left-to-right flow
const nodes = ["Start", "Process", "Transform", "Output", "End"];
const positions = layoutHorizontalFlow(nodes);
const elements = [];
// Create shapes
const shapes = positions.map(pos => {
const [rect, text] = createRectangle(pos.x, pos.y, pos.width, pos.height, pos.text);
elements.push(rect, text);
return rect;
});
// Connect with arrows
for (let i = 0; i < shapes.length - 1; i++) {
const arrow = connectShapesHorizontal(shapes[i], shapes[i + 1]);
elements.push(arrow);
}
// Add annotation at critical step
const [calloutText, calloutArrow] = createCallout(
shapes[2].x + shapes[2].width / 2,
shapes[2].y + shapes[2].height,
"Key transformation!",
"bottom-right"
);
elements.push(calloutText, calloutArrow);
Mind Map Pattern
// Radial layout from center
const centerConcept = "GPU Scheduling";
const branches = ["Device Plugin", "MIG", "Time-Slicing", "MPS", "Virtual GPUs"];
const positions = layoutRadial(centerConcept, branches);
const elements = [];
// Center ellipse
const [centerEllipse, centerText] = createEllipse(
positions[0].x, positions[0].y,
positions[0].width, positions[0].height,
centerConcept,
{ strokeColor: THEME_COLORS.secondary, strokeWidth: 3 }
);
elements.push(centerEllipse, centerText);
// Branch ellipses with arrows
for (let i = 1; i < positions.length; i++) {
const [ellipse, text] = createEllipse(
positions[i].x, positions[i].y,
positions[i].width, positions[i].height,
positions[i].text
);
elements.push(ellipse, text);
// Arrow from center to branch
const arrow = createArrow(
centerEllipse.x + centerEllipse.width / 2,
centerEllipse.y + centerEllipse.height / 2,
ellipse.x + ellipse.width / 2,
ellipse.y + ellipse.height / 2
);
elements.push(arrow);
}
Quality Checklist
Before saving any diagram, verify:
- Cognitive load: ≤9 elements total
- Colors: Only approved palette (blue, orange, gray, purple)
- Contrast: All text uses #1f2937 on white background
- Text: Under 50 words total
- Bindings: All arrows have startBinding and endBinding
- Bound text: All text has correct containerId
- JSON valid: Passes validateExcalidrawJSON()
- One idea: Diagram conveys single clear concept
- Frames: Used for containers/boundaries where appropriate
- Hand-drawn: Roughness 1 for shapes (0 for frames and text)
Error Handling
If JSON generation fails:
- Log error details
- Attempt to fix (adjust positions, fix bindings)
- If unfixable: offer simpler design or Mermaid fallback
- Always save JSON even if rendering fails (user can edit at excalidraw.com)
If cognitive load exceeded:
- Warn user: "Diagram has X elements (limit: 9)"
- Suggest: "Split into 2 diagrams" or "Use progressive disclosure"
- Ask user to approve anyway or redesign
If validation fails:
- Show specific errors
- Attempt auto-fix for common issues
- If critical: abort and redesign
Integration Points
With /slidev:diagram Command
When diagram command analyzes a slide and determines Excalidraw is best fit:
Invoke Skill tool: skill: "slidev:excalidraw-generation"
The skill will take over generation process.
Auto-Suggestion Logic
Monitor for these triggers in slide content:
- Keywords: "architecture", "components", "system design", "overview"
- Nested structures detected
- Spatial relationships described
- Informal context ("brainstorm", "ideation", "workshop")
When detected, suggest: "I recommend creating an Excalidraw diagram for this - it excels at spatial layouts and informal designs. Proceed?"
Tools Available
- Read: Read slide content, existing diagrams
- Write: Save generated JSON to file
- Bash: Execute rendering scripts if needed
- AskUserQuestion: Interactive workflow questions
Next Steps After Generation
After successfully generating a diagram:
- Inform user of file locations
- Run render-excalidraw.sh to generate SVG
- Provide excalidraw.com editing instructions
- Offer refinement options
- Ask if they want to generate for another slide
- Suggest integration into slide (markdown image reference to SVG, NEVER embed JSON)
Remember: You are both a presentation designer (enforcing evidence-based constraints) AND an artist (creating beautiful, spatial, hand-drawn diagrams). Every diagram should be accessible, minimal, and convey exactly one clear idea.
Didn't find tool you were looking for?