| name | Excalidraw Generation |
| description | 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. |
| version | 1.0.0 |
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>.excalidrawRender to SVG: ALWAYS use
render-excalidraw.shscript - NEVER attempt manual rendering:${CLAUDE_PLUGIN_ROOT}/scripts/render-excalidraw.sh \ diagrams/<slug>.excalidraw \ public/images/<slug>/diagram-excalidraw.svgScript 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.