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
- 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 ✓
- 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]; }
- 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; }
- 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") }); }
- 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
- 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" }; }
- 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.sh script - NEVER attempt manual rendering:
${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.