
Hand Drawn Diagrams
- 375 installs
- 57 repo stars
- Updated August 1, 2026
- muthuishere/hand-drawn-diagrams
hand-drawn-diagrams is an agent skill that turns natural-language prompts into hand-drawn Excalidraw architecture, workflow, and UX blueprint diagrams with hosted edit URLs, animations, and PNG or SVG exports for develop
About
hand-drawn-diagrams is an agent skill for Claude Code, Codex, and compatible agents that converts natural-language prompts into hand-drawn Excalidraw diagrams. It routes requests to diagram types for architecture, workflow, and UX blueprint visuals, generates non-overlapping layouts, validates output, and delivers a hosted edit URL plus diagram.excalidraw and diagram.animationinfo.json artifacts. Developers can export PNG or SVG, preview animations in the browser, and keep generated files under /tmp/hand-drawn-diagrams/ by default. Install with npx skills add muthuishere/hand-drawn-diagrams. Reach for hand-drawn-diagrams when you need sketch-style system diagrams, flow explainers, or wireframe blueprints without manually drawing every box and arrow in Excalidraw.
- hand-drawn-diagrams
- AI & Agent Building
- AI-coding skill
Hand Drawn Diagrams by the numbers
- 375 all-time installs (skills.sh)
- +23 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #2,065 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/muthuishere/hand-drawn-diagrams --skill hand-drawn-diagramsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 375 |
|---|---|
| repo stars | ★ 57 |
| Last updated | August 1, 2026 |
| Repository | muthuishere/hand-drawn-diagrams ↗ |
How do you generate Excalidraw diagrams from prompts?
Helps with ai & agent building tasks.
Who is it for?
Developers documenting system architecture, workflows, or UX flows who want sketch-style Excalidraw diagrams without manual canvas work.
Skip if: Teams needing polished corporate vector diagrams or CAD-precise technical drawings instead of hand-drawn Excalidraw sketches.
When should I use this skill?
A developer asks for an architecture diagram, workflow sketch, UX blueprint, or animated Excalidraw explainer from a natural-language description.
What you get
diagram.excalidraw file, diagram.animationinfo.json, hosted edit URL, and optional PNG or SVG diagram exports.
- diagram.excalidraw
- diagram.animationinfo.json
- PNG or SVG diagram export
By the numbers
- Outputs diagram.excalidraw and diagram.animationinfo.json as required artifacts
- Generates architecture, workflow, and UX blueprint diagram types
Files
Follow the instructions in ./workflow.md.
Key references:
references/index.mdreferences/activation-routing.xmlreferences/fundamental-shapes.md
Recommended: Chrome DevTools MCP
Install chrome-devtools-mcp for fast PNG and animated SVG rendering — uses a real browser, no Playwright install required.
npm install -g chrome-devtools-mcpThen add it to your Claude Code MCP config (~/.claude/settings.json):
{
"mcpServers": {
"chrome-devtools-mcp": {
"command": "npx",
"args": ["chrome-devtools-mcp"]
}
}
}Without it, PNG and video rendering falls back to Playwright (slower, requires browser install).
assets/*.gif filter=lfs diff=lfs merge=lfs -text
assets/*.mov filter=lfs diff=lfs merge=lfs -text
.setup_complete
*.log
node_modules
dist
.agents
.claude
.claude_cache
_bmad
.bmad_cache
_bmad-output
.github/skills
.opencode
design-artifacts
__pycache__
*.pyc
.venvAGENTS.md
Project: hand-drawn-diagrams
AI skill that converts natural language prompts into hand-drawn Excalidraw diagrams with hosted edit URLs, animated SVGs, and PNGs.
Agent Workflow
This skill follows a strict 3-step process. Never skip steps or reorder them.
Step 1 — Route (steps/step-01-route.md)
Pick one diagram type from the routing table:
| Route | Keywords |
|---|---|
teachers | teach, explain simply, lesson, compare, ELI5 |
ideation | brainstorm, cluster notes, opportunity map |
ux | journey, wireflow, sitemap, screen flow |
sales-funnel | funnel, drop-off, conversion, leads |
technical-explainer | architecture, API, protocol, event flow |
medical | condition, diagnosis, treatment, patient |
creative-raw | open/explicit creative mode |
page-mockup | webpage, dashboard, landing page, UI |
Load references/activation-routing.xml and references/fundamental-shapes.md first, then one route guide from references/.
Step 2 — Draw (steps/step-02-draw.md)
Write two files (in /tmp/hand-drawn-diagrams/<slug>/ by default): 1. diagram.excalidraw — complete .excalidraw JSON with non-empty elements array 2. diagram.animationinfo.json — animation spec with story pattern and element order
Both files must exist before Step 3 begins.
Step 3 — Validate & Deliver (steps/step-03-validate.md)
# 1. Validate first — fix all errors before proceeding
cd scripts && uv run python validate_excalidraw.py "/tmp/hand-drawn-diagrams/<slug>/diagram.excalidraw"
# 2. Generate hosted URLs only after validation passes
cd scripts && uv run python get_excalidraw_urls.py "/tmp/hand-drawn-diagrams/<slug>/diagram.excalidraw"Deliver the full Edit URL as a clickable link. Offer animation at the end.
Tool Usage
Rendering tools (always cd scripts first, use uv run)
| Tool | When to use |
|---|---|
validate_excalidraw.py | Always — before URL generation |
open_diagram.py | After validation — writes open.html, opens browser, prints short local path |
get_excalidraw_urls.py | Fallback only — prints raw hosted URLs when open_diagram.py unavailable |
edit_excalidraw.py | Open diagram directly in hosted editor (legacy) |
render_excalidraw.py | PNG export (Playwright fallback) |
render_animated_svg.py | Animated SVG (Playwright fallback) |
Chrome DevTools MCP (preferred over Playwright)
Use when available for PNG and animated SVG rendering:
mcp__plugin_chrome-devtools-mcp_chrome-devtools__new_pagemcp__plugin_chrome-devtools-mcp_chrome-devtools__navigate_pagemcp__plugin_chrome-devtools-mcp_chrome-devtools__wait_formcp__plugin_chrome-devtools-mcp_chrome-devtools__evaluate_script(extract SVG)mcp__plugin_chrome-devtools-mcp_chrome-devtools__take_screenshot(PNG)mcp__plugin_chrome-devtools-mcp_chrome-devtools__close_page
Excalidraw MCP (for checkpointing)
mcp__claude_ai_excalidraw__export_to_excalidrawmcp__claude_ai_excalidraw__create_viewmcp__claude_ai_excalidraw__save_checkpoint/read_checkpoint
File Location Rules
| Artifact | Default location |
|---|---|
.excalidraw | /tmp/hand-drawn-diagrams/<slug>/diagram.excalidraw |
.animationinfo.json | /tmp/hand-drawn-diagrams/<slug>/diagram.animationinfo.json |
.animated.svg | User's current project/workspace directory |
.png | User's current project/workspace directory |
Write to workspace only if the user specifies a path or explicitly asks.
Diagram Rules
- Monochrome by default — no fills unless essential for clarity
- Sketch font — same font across all elements
- 1–5 word labels — never write sentences or paragraphs inside shapes
- No floating arrows — bind with
startBinding/endBinding - Bind text to containers — use
containerId+boundElements - One concept per box — split instead of cramming
- Non-overlapping layout — use grid from
references/element-templates.md
Delivery Pattern
1. Run open_diagram.py — opens browser with open.html launcher. Deliver the printed local path as [Open diagram](file:///tmp/.../open.html). Never paste the raw hosted URL — it is too long to be usable. 2. Offer: "Want a video version? I can render it as an animated diagram (~10s)" 3. If in /tmp, offer: "Want me to save the source file to your project?" 4. PNG only if user explicitly asked for an image
Critical Rules
1. Never render before the `.excalidraw` file exists and validates clean 2. Never run `get_excalidraw_urls.py` before `validate_excalidraw.py` exits 0 3. Never write source files to the user's workspace without being asked 4. Always write `.animated.svg` to the user's workspace (it is the video deliverable) 5. Never skip or reorder the 3 steps — even if video was the original request
Testing
cd scripts
uv run pytest # full test suite
uv run pytest -m "not slow" # skip Playwright/Chromium browser testsReferences
references/index.md— index of all reference documentsreferences/activation-routing.xml— full routing rulesreferences/fundamental-shapes.md— shape vocabularyreferences/json-schema.md—.excalidrawJSON formatreferences/element-templates.md— grid layout + element templatesreferences/animation-spec.md— animation story patternsreferences/quality-checklist.md— diagram quality criteria
CLAUDE.md
Project Overview
hand-drawn-diagrams is an AI skill that converts natural language prompts into hand-drawn Excalidraw diagrams. It produces a hosted edit URL, animated SVG, and PNG — from a single prompt, no app required.
This repository contains the skill definition, rendering scripts, and installer for Claude Code and compatible agents.
Repository Structure
hand-drawn-diagrams/
├── SKILL.md # Skill entry point (name + description)
├── workflow.md # Master workflow — read this first
├── steps/
│ ├── step-01-route.md # Pick diagram type from routing table
│ ├── step-02-draw.md # Generate .excalidraw + .animationinfo.json
│ └── step-03-validate.md # Validate, get hosted URL, offer animation
├── references/
│ ├── activation-routing.xml # Route selection rules
│ ├── fundamental-shapes.md # Core shape language
│ ├── json-schema.md # .excalidraw JSON schema
│ ├── element-templates.md # Grid layout + element templates
│ ├── animation-spec.md # Animation story patterns
│ ├── quality-checklist.md # Diagram quality rules
│ ├── patterns.md # Reusable layout patterns
│ └── *-diagrams.md # Per-route guides (teachers, ux, funnel…)
├── scripts/ # Python rendering tools (managed with uv)
│ ├── validate_excalidraw.py # Pre-flight validation — run before URL gen
│ ├── get_excalidraw_urls.py # Generates hosted Edit + Animate URLs
│ ├── edit_excalidraw.py # Opens diagram in hosted editor
│ ├── render_excalidraw.py # PNG export (Playwright fallback)
│ ├── render_animated_svg.py # Animated SVG export (Playwright fallback)
│ ├── animate_excalidraw.py # Animation helper
│ ├── hosted_scene_urls.py # URL encoding utilities
│ ├── local_excalidraw_server.py
│ ├── pyproject.toml # Python deps: playwright>=1.40, pytest
│ └── tests/ # pytest test suite
├── installscripts/
│ ├── install.py # Detects Claude Code / Agent CLI, installs skill
│ └── uninstall.py
├── install.sh / install.cmd # Shell wrappers → installscripts/install.py
├── uninstall.sh / uninstall.cmd
└── docs/ # Additional documentationTech Stack
- Language: Python 3.11+
- Package manager:
uv(always useuv run python ...for scripts) - Rendering: Chrome DevTools MCP (preferred) or Playwright (fallback)
- Output format:
.excalidrawJSON,.animationinfo.json,.animated.svg,.png - Diagram host: Excalidraw hosted editor (gzip/base64 scene in URL hash)
Skill Activation
The skill is activated via SKILL.md. When activated, it follows workflow.md:
1. Route (step-01-route.md) — pick one diagram type 2. Draw (step-02-draw.md) — write .excalidraw + .animationinfo.json 3. Validate (step-03-validate.md) — validate, generate hosted URL, deliver
Key Conventions
File locations
.excalidraw+.animationinfo.json→/tmp/hand-drawn-diagrams/<slug>/(never litter workspace).animated.svg→ always the user's project/workspace directory- Write to workspace only if user specifies a path
Rendering commands (always run from scripts/)
cd scripts
uv run python validate_excalidraw.py "/absolute/path/to/file.excalidraw"
uv run python open_diagram.py "/absolute/path/to/file.excalidraw" # preferred: writes open.html + opens browser
uv run python get_excalidraw_urls.py "/absolute/path/to/file.excalidraw" # fallback: prints raw URLs
uv run python render_excalidraw.py "/absolute/path/to/file.excalidraw"
uv run python render_animated_svg.py "/tmp/.../diagram.excalidraw" --output "/project/name.animated.svg"Rendering priority
1. Chrome DevTools MCP — fast, uses real browser, no install required 2. Playwright scripts — fallback only
Output delivery order
1. Hosted edit URL (always first, after validation passes) 2. Offer animation ("Want a video version?") 3. PNG only if explicitly requested
Diagram style defaults
- Hand-drawn / sketch font
- Monochrome (no fills unless essential)
- Files go to
/tmp/— workspace stays clean
Running Tests
cd scripts
uv run pytest # all tests
uv run pytest -m "not slow" # skip Playwright/Chromium tests
uv run pytest tests/test_validate_excalidraw.pyInstallation / Uninstall
bash install.sh # macOS/Linux — detects Claude Code and Agent CLI
install.cmd # Windows
bash uninstall.sh # macOS/Linux
uninstall.cmd # WindowsWhat NOT to do
- Do not write
.excalidrawfiles to the user's workspace without being asked - Do not run
get_excalidraw_urls.pybeforevalidate_excalidraw.pyexits 0 - Do not render before the
.excalidrawfile is written and validated - Do not use
excalidraw.comdirectly — useedit_excalidraw.pyto get hosted URLs - Do not add multiple colors/fills for decoration — keep diagrams monochrome by default
- Do not write paragraphs inside shapes — diagrams use 1–5 word labels only
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hand Drawn Diagrams — Animate</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #f0f0f3;
font-family: system-ui, -apple-system, sans-serif;
display: flex;
flex-direction: column;
min-height: 100vh;
color: #1a1a2e;
}
/* ── Toolbar ── */
#toolbar {
background: #fff;
border-bottom: 1px solid #e4e4e8;
padding: 0 20px;
height: 52px;
display: flex;
align-items: center;
gap: 12px;
position: sticky;
top: 0;
z-index: 10;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
#brand {
display: flex;
align-items: center;
gap: 8px;
text-decoration: none;
}
#brand-name {
font-size: 14px;
font-weight: 700;
color: #1a1a2e;
letter-spacing: -0.3px;
}
#brand-tag {
font-size: 11px;
font-weight: 600;
background: #6965db;
color: #fff;
padding: 2px 7px;
border-radius: 20px;
letter-spacing: 0.2px;
}
#divider {
width: 1px;
height: 20px;
background: #e4e4e8;
}
#status {
font-size: 13px;
color: #888;
flex: 1;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 14px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: background 0.15s, opacity 0.15s;
white-space: nowrap;
}
.btn:disabled { opacity: 0.35; cursor: default; }
.btn-primary {
background: #6965db;
color: #fff;
}
.btn-primary:hover:not(:disabled) { background: #5a57c8; }
.btn-ghost {
background: #f4f4f6;
color: #444;
}
.btn-ghost:hover:not(:disabled) { background: #e8e8ec; }
/* ── Progress bar ── */
#progress-track {
height: 2px;
background: transparent;
display: none;
}
#progress-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #6965db, #a78bfa);
transition: width linear;
}
/* ── Canvas area ── */
#stage {
flex: 1;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 36px 24px;
}
#root svg {
max-width: 100%;
height: auto;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0,0,0,0.08), 0 1px 4px rgba(0,0,0,0.04);
}
/* ── Empty state ── */
#empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 80px 24px;
color: #aaa;
text-align: center;
}
#empty svg { opacity: 0.3; }
#empty p { font-size: 15px; line-height: 1.6; }
#empty code {
background: #eee;
color: #555;
padding: 2px 7px;
border-radius: 5px;
font-size: 12px;
}
</style>
</head>
<body>
<div id="toolbar">
<a id="brand" href="https://github.com/muthuishere/hand-drawn-diagrams" target="_blank">
<span id="brand-name">✍ Hand Drawn Diagrams</span>
<span id="brand-tag">animate</span>
</a>
<div id="divider"></div>
<span id="status">Ready</span>
<button id="downloadBtn" class="btn btn-primary" disabled>
↓ Download Animation
</button>
<button id="editBtn" class="btn btn-ghost"
onclick="window.location.href='edit.html' + window.location.hash">
✎ Edit
</button>
</div>
<div id="progress-track"><div id="progress-fill"></div></div>
<div id="stage">
<div id="root">
<div id="empty">
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="#6965db" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20h9"/>
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
</svg>
<p>No diagram loaded.<br>
Use <code>get_excalidraw_urls.py diagram.excalidraw</code> to open one here.</p>
</div>
</div>
</div>
<script type="module">
const statusEl = document.getElementById('status');
const rootEl = document.getElementById('root');
const downloadBtn = document.getElementById('downloadBtn');
const progressTrack = document.getElementById('progress-track');
const progressFill = document.getElementById('progress-fill');
let currentSvg = null;
// ── Decompress gzip+base64 hash ──────────────────────────────────────────
async function decompress(b64) {
const binary = atob(b64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
writer.write(bytes);
writer.close();
const chunks = [];
const reader = ds.readable.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const total = chunks.reduce((n, c) => n + c.length, 0);
const result = new Uint8Array(total);
let off = 0;
for (const c of chunks) { result.set(c, off); off += c.length; }
return new TextDecoder().decode(result);
}
// ── Unpack bundle — handles both old (raw excalidraw) and new ({excalidraw, animationInfo}) ──
function unpack(data) {
if (data.excalidraw) {
return { excalidrawData: data.excalidraw, animationInfo: data.animationInfo || null };
}
// Legacy: the payload IS the excalidraw JSON
return { excalidrawData: data, animationInfo: null };
}
// ── Apply animationInfo by injecting ID suffixes (mirrors Python apply_animation_info) ──
function applyAnimationInfo(elements, animationInfo, defaultDuration) {
if (!animationInfo) return elements;
const overrides = {};
for (const entry of (animationInfo.elements || [])) {
if (entry.id) overrides[entry.id] = entry;
}
return elements.map(el => {
const eid = el.id || '';
const ov = overrides[eid];
if (ov) {
const parts = [];
if ('order' in ov) parts.push(`animateOrder:${ov.order}`);
const dur = (ov.duration !== undefined) ? ov.duration : defaultDuration;
parts.push(`animateDuration:${dur}`);
return { ...el, id: eid + '-' + parts.join('-') };
}
return { ...el, id: `${eid}-animateDuration:${defaultDuration}` };
});
}
// ── Download animated SVG ────────────────────────────────────────────────
function downloadAnimation() {
if (!currentSvg) return;
const svgStr = new XMLSerializer().serializeToString(currentSvg);
const blob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'animation.svg';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
statusEl.textContent = 'Downloaded — open the .svg in any browser to play';
}
// ── Main ─────────────────────────────────────────────────────────────────
async function main() {
const hash = window.location.hash.slice(1);
if (!hash) return;
progressTrack.style.display = 'block';
progressFill.style.transition = 'width 0.4s ease';
progressFill.style.width = '20%';
statusEl.textContent = 'Decompressing…';
let excalidrawData, animationInfo;
try {
const raw = JSON.parse(await decompress(hash));
({ excalidrawData, animationInfo } = unpack(raw));
} catch (e) {
statusEl.textContent = `Decode error: ${e.message}`;
progressTrack.style.display = 'none';
return;
}
progressFill.style.width = '50%';
statusEl.textContent = 'Loading libraries…';
let exportToSvg, animateSvg;
try {
const [excalidrawMod, animateMod] = await Promise.all([
import('https://esm.sh/@excalidraw/excalidraw@0.18.0?bundle'),
import('https://esm.sh/excalidraw-animate@0.7.1?bundle'),
]);
exportToSvg = excalidrawMod.exportToSvg;
animateSvg = animateMod.animateSvg;
} catch (e) {
statusEl.textContent = `Library error: ${e.message}`;
progressTrack.style.display = 'none';
return;
}
progressFill.style.width = '80%';
statusEl.textContent = 'Rendering…';
const rawElements = (excalidrawData.elements || []).filter(e => !e.isDeleted);
const appState = excalidrawData.appState || {};
const files = excalidrawData.files || {};
const startMs = animationInfo?.startMs ?? 500;
const defaultDur = animationInfo?.defaultDuration ?? 500;
const elements = applyAnimationInfo(rawElements, animationInfo, defaultDur);
if (animationInfo) {
statusEl.textContent = `Rendering with animation spec (${(animationInfo.elements||[]).length} overrides)…`;
}
try {
const svg = await exportToSvg({
elements,
appState: { ...appState, exportBackground: true,
viewBackgroundColor: appState.viewBackgroundColor || '#ffffff',
exportWithDarkMode: false },
files,
});
const { finishedMs } = animateSvg(svg, elements, { startMs });
currentSvg = svg;
rootEl.innerHTML = '';
rootEl.appendChild(svg);
progressFill.style.width = '100%';
setTimeout(() => { progressTrack.style.display = 'none'; }, 400);
const secs = (finishedMs / 1000).toFixed(1);
statusEl.textContent = `Animating · ${secs}s`;
downloadBtn.disabled = false;
setTimeout(() => { statusEl.textContent = 'Done'; }, finishedMs);
} catch (e) {
statusEl.textContent = `Render error: ${e.message}`;
progressTrack.style.display = 'none';
console.error(e);
}
downloadBtn.addEventListener('click', downloadAnimation);
}
main().catch(e => { statusEl.textContent = `Error: ${e.message}`; console.error(e); });
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Hand Drawn Diagrams — Edit</title>
<script>
window.EXCALIDRAW_ASSET_PATH =
"https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/prod/";
</script>
<link
rel="stylesheet"
href="https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/dev/index.css"
/>
<style>
html, body, #root {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #ffffff;
}
#toolbar {
position: fixed;
top: 12px;
right: 12px;
z-index: 20;
display: flex;
gap: 8px;
align-items: center;
padding: 8px 12px;
border: 1px solid #e4e4e8;
border-radius: 12px;
background: rgba(255, 255, 255, 0.97);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.10);
font: 13px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
#toolbar-brand {
display: flex;
align-items: center;
gap: 6px;
padding-right: 8px;
border-right: 1px solid #e4e4e8;
margin-right: 2px;
text-decoration: none;
}
#toolbar-brand-name {
font-size: 13px;
font-weight: 700;
color: #1a1a2e;
}
#toolbar-brand-tag {
font-size: 10px;
font-weight: 600;
background: #6965db;
color: #fff;
padding: 2px 6px;
border-radius: 20px;
}
#toolbar button {
border: 1px solid #e0e0e4;
border-radius: 8px;
background: #ffffff;
color: #1e1e1e;
padding: 7px 13px;
cursor: pointer;
font: inherit;
font-weight: 500;
transition: background 0.15s;
}
#toolbar button:hover:not(:disabled) { background: #f4f4f6; }
#toolbar button:disabled { opacity: 0.35; cursor: default; }
#toolbar button.primary {
background: #6965db;
color: #fff;
border-color: #6965db;
}
#toolbar button.primary:hover:not(:disabled) { background: #5a57c8; }
#status {
color: #888;
white-space: nowrap;
font-size: 12px;
}
#loading-overlay {
position: fixed;
inset: 0;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
flex-direction: column;
gap: 12px;
}
#loading-overlay .loading-brand {
font-size: 18px;
font-weight: 700;
color: #1a1a2e;
letter-spacing: -0.3px;
}
#loading-overlay .loading-brand span {
background: #6965db;
color: #fff;
font-size: 11px;
font-weight: 600;
padding: 2px 7px;
border-radius: 20px;
margin-left: 6px;
vertical-align: middle;
}
#loading-msg {
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #888;
}
#loading-overlay.hidden { display: none; }
</style>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@19.0.0",
"react/jsx-runtime": "https://esm.sh/react@19.0.0/jsx-runtime",
"react-dom": "https://esm.sh/react-dom@19.0.0",
"react-dom/client": "https://esm.sh/react-dom@19.0.0/client"
}
}
</script>
</head>
<body>
<div id="loading-overlay">
<div class="loading-brand">✍ Hand Drawn Diagrams<span>edit</span></div>
<span id="loading-msg">Loading…</span>
</div>
<div id="root"></div>
<div id="toolbar">
<a id="toolbar-brand" href="https://github.com/muthuishere/hand-drawn-diagrams" target="_blank">
<span id="toolbar-brand-name">✍ Hand Drawn Diagrams</span>
<span id="toolbar-brand-tag">edit</span>
</a>
<button id="animate-btn" type="button" class="primary" disabled>▶ Animate</button>
<button id="download-btn" type="button" disabled>↓ Download</button>
<span id="status">Loading…</span>
</div>
<script type="module">
import React from "https://esm.sh/react@19.0.0";
import ReactDOMClient from "https://esm.sh/react-dom@19.0.0/client";
import * as ExcalidrawLib from "https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/dev/index.js?external=react,react-dom,react-dom/client";
const { Excalidraw, serializeAsJSON, MIME_TYPES } = ExcalidrawLib;
const overlay = document.getElementById('loading-overlay');
const loadingMsg = document.getElementById('loading-msg');
const statusEl = document.getElementById('status');
const downloadBtn = document.getElementById('download-btn');
const animateBtn = document.getElementById('animate-btn');
// ── Compress current scene → gzip+base64 hash ────────────────────────────
async function compress(jsonStr) {
const bytes = new TextEncoder().encode(jsonStr);
const cs = new CompressionStream('gzip');
const writer = cs.writable.getWriter();
writer.write(bytes);
writer.close();
const chunks = [];
const reader = cs.readable.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const total = chunks.reduce((n, c) => n + c.length, 0);
const result = new Uint8Array(total);
let off = 0;
for (const c of chunks) { result.set(c, off); off += c.length; }
// btoa requires a binary string
let binary = '';
for (const byte of result) binary += String.fromCharCode(byte);
return btoa(binary);
}
// ── Decompress gzip+base64 hash ──────────────────────────────────────────
async function decompress(b64) {
const binary = atob(b64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
writer.write(bytes);
writer.close();
const chunks = [];
const reader = ds.readable.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const total = chunks.reduce((n, c) => n + c.length, 0);
const result = new Uint8Array(total);
let off = 0;
for (const c of chunks) { result.set(c, off); off += c.length; }
return new TextDecoder().decode(result);
}
// ── Unpack bundle — handles both old (raw excalidraw) and new ({excalidraw, animationInfo}) ──
function unpack(data) {
if (data.excalidraw) {
return { excalidrawData: data.excalidraw, animationInfo: data.animationInfo || null };
}
return { excalidrawData: data, animationInfo: null };
}
// ── Main ─────────────────────────────────────────────────────────────────
async function main() {
const hash = window.location.hash.slice(1);
if (!hash) {
loadingMsg.textContent = 'No diagram in URL. Use the launcher script to open a file here.';
return;
}
// Decode
let excalidrawData, animationInfo;
try {
loadingMsg.textContent = 'Decompressing...';
const raw = JSON.parse(await decompress(hash));
({ excalidrawData, animationInfo } = unpack(raw));
} catch (e) {
loadingMsg.textContent = `Decode error: ${e.message}`;
return;
}
const initialData = {
elements: excalidrawData.elements || [],
appState: excalidrawData.appState || {},
files: excalidrawData.files || {},
scrollToContent: true,
};
loadingMsg.textContent = 'Starting editor...';
// Download handler — exports current canvas as .excalidraw
let excalidrawAPI = null;
downloadBtn.addEventListener('click', () => {
if (!excalidrawAPI) return;
const serialized = serializeAsJSON(
excalidrawAPI.getSceneElementsIncludingDeleted(),
excalidrawAPI.getAppState(),
excalidrawAPI.getFiles(),
'local',
);
const blob = new Blob([serialized], { type: MIME_TYPES.excalidraw });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'diagram.excalidraw';
a.click();
statusEl.textContent = 'Downloaded';
});
// Animate button — bundle current canvas + original animationInfo → animate.html
animateBtn.addEventListener('click', async () => {
if (!excalidrawAPI) return;
animateBtn.disabled = true;
statusEl.textContent = 'Encoding...';
try {
const sceneJson = serializeAsJSON(
excalidrawAPI.getSceneElementsIncludingDeleted(),
excalidrawAPI.getAppState(),
excalidrawAPI.getFiles(),
'local',
);
const bundle = { excalidraw: JSON.parse(sceneJson) };
if (animationInfo) bundle.animationInfo = animationInfo;
const newHash = await compress(JSON.stringify(bundle));
window.location.href = 'animate.html#' + newHash;
} catch (e) {
statusEl.textContent = `Error: ${e.message}`;
animateBtn.disabled = false;
}
});
// Render
ReactDOMClient.createRoot(document.getElementById('root')).render(
React.createElement(
'div',
{ style: { width: '100%', height: '100%' } },
React.createElement(Excalidraw, {
initialData,
excalidrawAPI: (api) => {
excalidrawAPI = api;
overlay.classList.add('hidden');
downloadBtn.disabled = false;
animateBtn.disabled = false;
statusEl.textContent = 'Ready';
// Fit all elements into view once rendering settles
setTimeout(() => {
api.scrollToContent(api.getSceneElements(), { fitToViewport: true });
}, 150);
},
}),
),
);
}
main().catch(e => {
loadingMsg.textContent = `Error: ${e.message}`;
console.error(e);
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hand Drawn Diagrams</title>
<meta name="description" content="Hosted editor and animation pages for the Hand Drawn Diagrams skill." />
<style>
:root {
--paper: #f6f0e6;
--ink: #1f1b17;
--muted: #665f57;
--card: rgba(255, 252, 246, 0.82);
--line: rgba(31, 27, 23, 0.14);
--accent: #2f6b57;
--accent-2: #bb6b3d;
--shadow: 0 18px 60px rgba(42, 32, 20, 0.12);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
color: var(--ink);
background:
radial-gradient(circle at top left, rgba(187, 107, 61, 0.16), transparent 28%),
radial-gradient(circle at right 18%, rgba(47, 107, 87, 0.14), transparent 24%),
linear-gradient(180deg, #fbf6ed 0%, var(--paper) 100%);
font-family: Georgia, "Times New Roman", serif;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0.28;
background-image:
linear-gradient(rgba(31, 27, 23, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(31, 27, 23, 0.03) 1px, transparent 1px);
background-size: 100% 38px, 38px 100%;
}
.wrap {
width: min(1080px, calc(100% - 32px));
margin: 0 auto;
padding: 40px 0 56px;
}
.hero {
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 24px;
align-items: stretch;
}
.panel {
background: var(--card);
border: 1px solid var(--line);
border-radius: 28px;
box-shadow: var(--shadow);
backdrop-filter: blur(8px);
}
.intro {
padding: 34px;
position: relative;
overflow: hidden;
}
.eyebrow {
display: inline-block;
padding: 6px 12px;
border-radius: 999px;
border: 1px solid var(--line);
color: var(--muted);
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
background: rgba(255, 255, 255, 0.48);
}
h1 {
margin: 18px 0 14px;
font-size: clamp(2.6rem, 5vw, 4.6rem);
line-height: 0.95;
letter-spacing: -0.05em;
}
.lede {
margin: 0;
max-width: 56ch;
color: var(--muted);
font-size: 1.06rem;
line-height: 1.7;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 28px;
}
.action {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 52px;
padding: 0 18px;
border-radius: 16px;
border: 1px solid var(--line);
text-decoration: none;
color: var(--ink);
font-size: 0.98rem;
background: rgba(255, 255, 255, 0.66);
}
.action.primary {
background: var(--accent);
color: #f8f6f1;
border-color: transparent;
}
.notes {
padding: 24px;
display: grid;
gap: 14px;
}
.note {
padding: 18px;
border-radius: 18px;
border: 1px dashed var(--line);
background: rgba(255, 255, 255, 0.5);
}
.note strong {
display: block;
margin-bottom: 6px;
font-size: 1rem;
}
.note p {
margin: 0;
color: var(--muted);
line-height: 1.6;
}
.footer {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 10px;
margin-top: 18px;
color: var(--muted);
font-size: 0.92rem;
}
code {
padding: 2px 7px;
border-radius: 999px;
background: rgba(31, 27, 23, 0.07);
font-family: "SFMono-Regular", Menlo, monospace;
font-size: 0.9em;
}
@media (max-width: 840px) {
.hero {
grid-template-columns: 1fr;
}
.wrap {
width: min(100% - 20px, 1080px);
padding-top: 20px;
}
.intro,
.notes {
padding: 22px;
}
}
</style>
</head>
<body>
<script>
if (window.location.hash) {
window.location.replace("./edit.html" + window.location.hash);
}
</script>
<main class="wrap">
<section class="hero">
<article class="panel intro">
<span class="eyebrow">Hosted Skill Pages</span>
<h1>Hand Drawn Diagrams</h1>
<p class="lede">
This GitHub Pages site hosts the editor and animator used by the skill.
Skill scripts encode the diagram into the URL hash, then open these pages
directly in the browser.
</p>
<div class="actions">
<a class="action primary" href="./edit.html">Open Editor</a>
<a class="action" href="./animate.html">Open Animator</a>
<a class="action" href="https://github.com/muthuishere/hand-drawn-diagrams">View Repository</a>
</div>
</article>
<aside class="panel notes">
<section class="note">
<strong>Editor</strong>
<p>Use <code>/docs/edit.html</code> for interactive editing and downloading the current <code>.excalidraw</code> file.</p>
</section>
<section class="note">
<strong>Animator</strong>
<p>Use <code>/docs/animate.html</code> to preview the sketch animation and download the animated SVG.</p>
</section>
<section class="note">
<strong>Hash Redirect</strong>
<p>If a diagram hash lands on the site root, this page forwards it to the editor automatically.</p>
</section>
</aside>
</section>
<div class="footer">
<span>GitHub Pages source: <code>main</code> branch, <code>/docs</code> folder</span>
<span>Repo: <a href="https://github.com/muthuishere/hand-drawn-diagrams">muthuishere/hand-drawn-diagrams</a></span>
</div>
</main>
</body>
</html>
@echo off
setlocal
set REPO_DIR=%~dp0
if "%REPO_DIR:~-1%"=="\" set REPO_DIR=%REPO_DIR:~0,-1%
set INSTALLER=%REPO_DIR%\installscripts\install.py
where py >nul 2>nul
if not errorlevel 1 (
py -3 "%INSTALLER%" %*
exit /b %errorlevel%
)
where python >nul 2>nul
if not errorlevel 1 (
python "%INSTALLER%" %*
exit /b %errorlevel%
)
echo Python is required to run the installer.
echo Install Python and run this script again.
exit /b 1
Installation
Jump to: npx skills add · install.sh · Manual · Uninstall
Prerequisites
- Python 3.11+ —
python3 --version - git —
git --version - uv —
pip install uvorbrew install uv - Node.js + npm (optional) — for fast PNG/video rendering via
chrome-devtools-mcp
---
npx skills add
The recommended way. Works for 40+ agents and handles project vs global scope.
# Project scope — committed with your repo, shared with team
npx skills add muthuishere/hand-drawn-diagrams
# Global scope — available in every project
npx skills add muthuishere/hand-drawn-diagrams -g
# Specific agents only
npx skills add muthuishere/hand-drawn-diagrams -a claude-code -a opencode| Agent | Project path | Global path |
|---|---|---|
| Claude Code | .claude/skills/ | ~/.claude/skills/ |
| Codex | .agents/skills/ | ~/.codex/skills/ |
| OpenCode | .agents/skills/ | ~/.config/opencode/skills/ |
| Windsurf | .windsurf/skills/ | ~/.codeium/windsurf/skills/ |
| GitHub Copilot | .agents/skills/ | ~/.copilot/skills/ |
| Cursor | .agents/skills/ | ~/.cursor/skills/ |
| Gemini CLI | .agents/skills/ | ~/.gemini/skills/ |
Full agent list at vercel-labs/skills.
---
install.sh
For agents that follow the Claude / agent skills directory spec. Global scope only — installs based on which agent CLIs are detected on your PATH.
macOS / Linux
git clone https://github.com/muthuishere/hand-drawn-diagrams.git
cd hand-drawn-diagrams
bash install.shWindows
git clone https://github.com/muthuishere/hand-drawn-diagrams.git
cd hand-drawn-diagrams
install.cmd---
Manual
For any agent not listed above, or if you prefer not to use npm. See MANUAL-INSTALL.md for per-agent copy commands and both project and global paths.
---
Uninstall
npx skills
npx skills remove hand-drawn-diagrams # project
npx skills remove hand-drawn-diagrams -g # globalinstall.sh (macOS / Linux)
bash uninstall.shinstall.sh (Windows)
uninstall.cmdManual — delete the directory you copied to, e.g.:
rm -rf ~/.claude/skills/hand-drawn-diagrams # global
rm -rf .claude/skills/hand-drawn-diagrams # project---
Optional: fast rendering
Install chrome-devtools-mcp for fast PNG and animated SVG export (real browser, no Playwright needed):
npm install -g chrome-devtools-mcpFor Claude Code, add to ~/.claude/settings.json:
{
"mcpServers": {
"chrome-devtools": {
"command": "chrome-devtools-mcp"
}
}
}---
Troubleshooting
| Symptom | Fix |
|---|---|
uv: command not found | pip install uv or brew install uv |
python: requires >=3.11 | Install Python 3.11+ and ensure it is first on PATH |
| Skill not found after install | Restart the agent / IDE session |
| Validation fails | Read the error from validate_excalidraw.py — usually a malformed JSON field |
| No hosted URL | Run get_excalidraw_urls.py only after validation exits 0 |
| Slow PNG render | Install chrome-devtools-mcp — Playwright fallback is much slower |
#!/bin/sh
set -e
REPO_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
SKILL_NAME="hand-drawn-diagrams"
for BASE in "$HOME/.agents/skills" "$HOME/.claude/skills"; do
mkdir -p "$BASE"
LINK="$BASE/$SKILL_NAME"
rm -rf "$LINK"
ln -s "$REPO_DIR" "$LINK"
echo "✓ linked $LINK -> $REPO_DIR"
done
#!/usr/bin/env python3
from __future__ import annotations
import os
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path
SKILL_NAME = "hand-drawn-diagrams"
@dataclass(frozen=True)
class InstallTarget:
key: str
label: str
cli_names: tuple[str, ...]
relative_dir: str
required: bool = False
def target(self, home_dir: Path) -> Path:
return home_dir / self.relative_dir / SKILL_NAME
def detected(self) -> bool:
return any(shutil.which(name) for name in self.cli_names)
TARGETS = (
InstallTarget("agents", "Agent Skills", ("codex",), ".agents/skills", required=False),
InstallTarget("claude", "Claude Code", ("claude",), ".claude/skills", required=False),
)
def repo_dir() -> Path:
return Path(__file__).resolve().parent.parent
def skill_dir() -> Path:
return repo_dir()
def setup_marker(scripts_dir: Path) -> Path:
return scripts_dir / ".setup_complete"
def render_venv_dir(scripts_dir: Path) -> Path:
return scripts_dir / ".venv"
def home_dir() -> Path:
if os.name == "nt":
return Path(os.environ.get("USERPROFILE", str(Path.home())))
return Path(os.environ.get("HOME", str(Path.home())))
def find_python_command() -> tuple[str | None, tuple[int, int, int] | None]:
candidates = ["python3", "python"] if os.name != "nt" else ["python", "py -3", "py"]
for candidate in candidates:
try:
completed = subprocess.run(
f'{candidate} -c "import sys; print(\'.\'.join(map(str, sys.version_info[:3])))"',
shell=True,
check=True,
capture_output=True,
text=True,
)
except subprocess.CalledProcessError:
continue
version_text = completed.stdout.strip()
try:
version = tuple(int(part) for part in version_text.split("."))
except ValueError:
continue
return candidate, version # type: ignore[return-value]
return None, None
def uv_status() -> tuple[bool, str]:
uv_cmd = shutil.which("uv")
if not uv_cmd:
return False, "missing"
completed = subprocess.run([uv_cmd, "--version"], capture_output=True, text=True, check=False)
version = completed.stdout.strip() or completed.stderr.strip() or "available"
return True, f"available ({version})"
def chrome_devtools_mcp_status() -> tuple[bool, str]:
"""Check if chrome-devtools-mcp is installed (enables fast browser rendering — no Playwright needed)."""
# Check npm global packages
npm_cmd = shutil.which("npm")
if npm_cmd:
result = subprocess.run(
[npm_cmd, "list", "-g", "--depth=0", "chrome-devtools-mcp"],
capture_output=True, text=True, check=False,
)
if "chrome-devtools-mcp" in result.stdout:
# Extract version
for line in result.stdout.splitlines():
if "chrome-devtools-mcp" in line:
version = line.strip().split("@")[-1] if "@" in line else "installed"
return True, f"installed ({version})"
# Check npx can resolve it (works if installed locally or globally via npx cache)
npx_cmd = shutil.which("npx")
if npx_cmd:
result = subprocess.run(
[npx_cmd, "--yes=false", "chrome-devtools-mcp", "--version"],
capture_output=True, text=True, check=False, timeout=5,
)
if result.returncode == 0:
version = result.stdout.strip() or "available"
return True, f"available via npx ({version})"
return False, "not found — install with: npm install -g chrome-devtools-mcp"
def detected_targets() -> list[InstallTarget]:
targets = [target for target in TARGETS if target.required or target.detected()]
if targets:
return targets
return [TARGETS[0]]
def capability_summary(uv_ready: bool, python_version: tuple[int, int, int] | None, setup_ready: bool) -> str:
python_ready = bool(python_version and python_version >= (3, 11))
if uv_ready and python_ready and setup_ready:
return "install + render ready"
if uv_ready and python_ready:
return "install ready, render will self-setup on first use"
return "skill install works, render setup blocked until missing prerequisites are installed"
def print_environment_summary(home: Path, selected_targets: list[InstallTarget]) -> None:
repo_scripts_dir = skill_dir() / "scripts"
print()
print("Detected install targets:")
for target in TARGETS:
cli_text = ", ".join(target.cli_names)
detected = "yes" if target in selected_targets else "no"
existing = "installed" if target.target(home).is_dir() else "not installed"
print(f" {target.label}: cli [{cli_text}] detected={detected}, target={existing}")
if selected_targets == [TARGETS[0]] and not TARGETS[0].detected():
print(" Fallback: no supported CLI detected, using ~/.agents/skills as the default shared location")
print()
print("Environment verification:")
uv_ready, uv_text = uv_status()
print(f" uv: {uv_text}")
python_cmd, python_version = find_python_command()
if python_cmd and python_version:
version_text = ".".join(str(part) for part in python_version)
if python_version >= (3, 11):
print(f" python: available via {python_cmd} ({version_text}, meets >=3.11)")
else:
print(f" python: available via {python_cmd} ({version_text}, requires >=3.11)")
else:
print(" python: missing")
if render_venv_dir(repo_scripts_dir).is_dir():
print(" render env: virtualenv present")
else:
print(" render env: virtualenv not created yet")
marker_present = setup_marker(repo_scripts_dir).is_file()
print(f" render setup marker: {'present' if marker_present else 'missing'}")
mcp_ready, mcp_text = chrome_devtools_mcp_status()
print(f" chrome-devtools-mcp: {mcp_text}")
if not mcp_ready:
print(" ↳ Recommended for fast PNG/video rendering (no Playwright install needed)")
print(" ↳ Configure in Claude Code: add chrome-devtools-mcp to your MCP servers")
print()
print(f"Capability summary: {capability_summary(uv_ready, python_version, marker_present)}")
if mcp_ready:
print(" chrome-devtools-mcp: rendering will use real browser (fast, no install needed)")
else:
print(" chrome-devtools-mcp: not found — PNG/video rendering will fall back to Playwright")
def install_target(target: InstallTarget, home: Path) -> None:
destination = target.target(home)
if destination.is_dir():
shutil.rmtree(destination)
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(
skill_dir(),
destination,
ignore=shutil.ignore_patterns(".venv", ".setup_complete", "__pycache__", ".DS_Store"),
)
print(f"✓ {target.label} -> {destination}")
def main() -> int:
home = home_dir()
selected_targets = detected_targets()
print_environment_summary(home, selected_targets)
print()
print("Installing skill to:")
for target in selected_targets:
print(f" - {target.label}")
print()
for target in selected_targets:
install_target(target, home)
print()
print("Render setup is deferred.")
print("The first PNG render will self-install its browser dependency if needed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
from __future__ import annotations
import os
import shutil
from dataclasses import dataclass
from pathlib import Path
SKILL_NAME = "hand-drawn-diagrams"
@dataclass(frozen=True)
class UninstallTarget:
label: str
relative_dir: str
def target(self, home_dir: Path) -> Path:
return home_dir / self.relative_dir / SKILL_NAME
TARGETS = (
UninstallTarget("Agent Skills", ".agents/skills"),
UninstallTarget("Claude Code", ".claude/skills"),
)
def repo_dir() -> Path:
return Path(__file__).resolve().parent.parent
def setup_marker() -> Path:
return repo_dir() / "scripts" / ".setup_complete"
def home_dir() -> Path:
if os.name == "nt":
return Path(os.environ.get("USERPROFILE", str(Path.home())))
return Path(os.environ.get("HOME", str(Path.home())))
def main() -> int:
home = home_dir()
found = False
print()
print(f"Uninstalling {SKILL_NAME}...")
print()
for target in TARGETS:
destination = target.target(home)
if destination.is_dir():
shutil.rmtree(destination)
print(f"✓ Removed {target.label} -> {destination}")
found = True
else:
print(f"- {target.label} -> not installed, skipping")
print()
if not found:
print("Nothing to uninstall.")
marker = setup_marker()
if marker.is_file():
marker.unlink()
print("✓ Cleaned up setup marker")
return 0
if __name__ == "__main__":
raise SystemExit(main())
MIT License
Copyright (c) 2025 Muthukumaran Navaneethakrishnan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
This project uses and builds upon the following open-source projects:
Excalidraw (https://github.com/excalidraw/excalidraw)
MIT License — Copyright (c) 2020 Excalidraw
excalidraw-animate (https://github.com/dai-shi/excalidraw-animate)
MIT License — Copyright (c) 2021 Daishi Kato
Manual Installation
For agents not supported by install.sh or npx skills add, copy the skill files directly into the agent's skills directory.
Step 1 — Clone
git clone https://github.com/muthuishere/hand-drawn-diagrams.git ~/.hand-drawn-diagramsStep 2 — Copy to the agent's skills directory
Global (available in every project)
| Agent | Path |
|---|---|
| Claude Code | ~/.claude/skills/hand-drawn-diagrams/ |
| Codex | ~/.codex/skills/hand-drawn-diagrams/ |
| OpenCode | ~/.config/opencode/skills/hand-drawn-diagrams/ |
| Windsurf | ~/.codeium/windsurf/skills/hand-drawn-diagrams/ |
| GitHub Copilot | ~/.copilot/skills/hand-drawn-diagrams/ |
| Cursor | ~/.cursor/skills/hand-drawn-diagrams/ |
| Gemini CLI | ~/.gemini/skills/hand-drawn-diagrams/ |
# Example: Claude Code global
mkdir -p ~/.claude/skills
cp -r ~/.hand-drawn-diagrams ~/.claude/skills/hand-drawn-diagramsProject (committed with your repo, shared with team)
| Agent | Path |
|---|---|
| Claude Code | .claude/skills/hand-drawn-diagrams/ |
| Codex | .agents/skills/hand-drawn-diagrams/ |
| OpenCode | .agents/skills/hand-drawn-diagrams/ |
| Windsurf | .windsurf/skills/hand-drawn-diagrams/ |
| GitHub Copilot | .agents/skills/hand-drawn-diagrams/ |
| Cursor | .agents/skills/hand-drawn-diagrams/ |
# Example: Claude Code project
mkdir -p .claude/skills
cp -r ~/.hand-drawn-diagrams .claude/skills/hand-drawn-diagramsCommit the directory to share the skill with your team.
Uninstall
Delete the directory you copied to:
# Claude Code global
rm -rf ~/.claude/skills/hand-drawn-diagrams
# Claude Code project
rm -rf .claude/skills/hand-drawn-diagramsTroubleshooting
| Symptom | Fix |
|---|---|
uv: command not found | pip install uv or brew install uv |
python: requires >=3.11 | Install Python 3.11+ and ensure it is first on PATH |
| Skill not found after install | Restart the agent / IDE session |
| Validation fails | Read the error from validate_excalidraw.py — usually a malformed JSON field |
| No hosted URL | Run get_excalidraw_urls.py only after validation exits 0 |
| Slow PNG render | Install chrome-devtools-mcp — Playwright fallback is much slower |
hand-drawn-diagrams
AI skill for turning ideas, notes, systems, and flows into hand-drawn diagrams — with a hosted edit URL, animated video, and PNG, all from a single prompt.
Jump to: What this is · Output · Quick start · Credits
!Demo
Example output
Static PNG — exported on request:
!Example diagram
Animated SVG — draws itself stroke by stroke (view):
!Exam revision diagram
What this is
hand-drawn-diagrams is an AI skill (for Claude Code, Codex CLI, and compatible agents) that takes a natural language prompt and produces a hand-drawn diagram you can edit, animate, and share — without opening any app.
You describe what you want. The AI picks the right diagram type, draws it in Excalidraw's sketch style, validates the layout, and hands you a live hosted URL. From there you can edit it in a browser, watch it animate, download the source, or export a PNG.
How it's different from using Excalidraw directly
| Excalidraw | hand-drawn-diagrams | |
|---|---|---|
| Starting point | Blank canvas, you draw | Natural language prompt |
| Diagram type | You decide | AI picks the right route (teaching, UX, architecture, funnel…) |
| Layout | You position everything | AI assigns non-overlapping coordinates |
| Animation | Manual or none | Auto-generated animation spec, renders in browser |
| Output | File on disk | Hosted edit URL + animated SVG + PNG on request |
| Workspace | You open the app | Files stay in /tmp/ by default — workspace stays clean |
This skill is not a replacement for Excalidraw — it sits on top of it. Every diagram it produces is a standard .excalidraw file you can open, edit, and own.
Best for
- students: study notes and exam revision maps
- teachers: lesson explainers and concept breakdowns
- architects: system and API flow diagrams
- builders: sequence diagrams and integration maps
- designers: UX flows and wireframes
- product: idea maps and feature flows
- sales: funnel and conversion visuals
- doctors: process and patient-facing explainers
Output
The agent infers what you want from your prompt and routes to the right output automatically:
| What you ask for | What you get |
|---|---|
| "create a diagram" / default | Browser opens to hosted Excalidraw editor — edit, tweak, download |
| "open the animation" | Browser opens animated view — diagram draws itself stroke by stroke |
| "save as excalidraw" | .excalidraw source file saved to your project |
| "save the animation" | .animated.svg saved to your project — plays in any browser |
| "save image" | .png saved to your project |
| "show image" | .png rendered and opened with your system viewer |
Source files go to /tmp/hand-drawn-diagrams/ by default — your workspace stays clean.
Quick start
npx skills add muthuishere/hand-drawn-diagramsWorks for Claude Code, Codex, OpenCode, Windsurf, GitHub Copilot, Cursor, Gemini CLI, and 40+ more agents. Detects which agents you have installed automatically.
For all install options, global vs project scope, and uninstall — see INSTALL.md.
Credits and acknowledgements
This skill stands on the shoulders of excellent open-source work:
- [Excalidraw](https://excalidraw.com/) — the open-source virtual whiteboard that powers the hand-drawn visual style and the hosted editor. All diagrams produced by this skill are standard Excalidraw files.
GitHub: excalidraw/excalidraw
- [excalidraw-animate](https://github.com/dai-shi/excalidraw-animate) by @dai-shi — the animation library that renders Excalidraw diagrams as SVGs that draw themselves stroke by stroke. The animated SVG output in this skill is powered by this library.
License
MIT — see LICENSE.
<activation-routing skill="hand-drawn-diagrams" version="2.0">
<defaults>
<rule>Pick one primary route.</rule>
<rule>Default to monochrome hand-drawn output.</rule>
<rule>Keep the same sketch font.</rule>
<rule>Use fundamental-shapes.md as the shared visual grammar.</rule>
<rule>Allow restrained color only for page-mockup when the user explicitly wants webpage-like fidelity.</rule>
<rule>Do not leave empty placeholder boxes or decorative empty sections in the final diagram.</rule>
<rule>Do not repeat the same long text across multiple regions; if text wraps badly or duplicates, simplify or remove it.</rule>
</defaults>
<diagram-rules>
<rule id="diagram-vs-notes">A diagram shows structure. Notes show content. Never write sentences or long bullet lists inside boxes — that is notes, not a diagram.</rule>
<rule id="label-length">Each shape label must be 1–5 words. If you need more, add another shape.</rule>
<rule id="one-concept-per-box">One concept per shape. If a topic has 10 concepts, use 10 shapes — not 1 shape with 10 bullets.</rule>
<rule id="bullets">Max 3 short bullets per container, each 5 words or fewer. No paragraphs inside shapes.</rule>
</diagram-rules>
<delivery-modes>
<rule>Infer the mode from the first prompt. Pass it as --mode to open_diagram.py in step-03. Never render before the .excalidraw file is written and validated.</rule>
<mode id="edit" open_diagram_flag="--mode edit" trigger="default; show me; open; just a diagram; no explicit output format">
<action>open_diagram.py --mode edit → writes open-edit.html (instant redirect to hosted Excalidraw editor) → opens in browser</action>
</mode>
<mode id="animate" open_diagram_flag="--mode animate" trigger="open animation; watch it animate; show animation; view animated">
<action>open_diagram.py --mode animate → writes open-animate.html (instant redirect to hosted animation view) → opens in browser</action>
</mode>
<mode id="save-excalidraw" open_diagram_flag="--mode save-excalidraw --dest {project}" trigger="save the file; save as excalidraw; keep the source; download the diagram">
<action>open_diagram.py --mode save-excalidraw → copies .excalidraw (and .animationinfo.json if present) to project directory</action>
</mode>
<mode id="save-animation" open_diagram_flag="--mode save-animation --dest {project}" trigger="save animation; animation video; animated SVG; save video">
<action>open_diagram.py --mode save-animation → renders and saves .animated.svg to project directory (always workspace, never /tmp)</action>
</mode>
<mode id="save-image" open_diagram_flag="--mode save-image --dest {project}" trigger="save image; export PNG; save PNG; export image">
<action>open_diagram.py --mode save-image → renders and saves .png to project directory</action>
</mode>
<mode id="open-image" open_diagram_flag="--mode open-image --dest {project}" trigger="show image; open image; open PNG; view image">
<action>open_diagram.py --mode open-image → renders .png, saves to project directory, opens with system viewer</action>
</mode>
</delivery-modes>
<routes>
<route id="teachers" reference="references/teachers-diagrams.md">
<when>teach, explain simply, lesson, compare, ELI5</when>
<goal>clarity and retention</goal>
<layout>top-to-bottom</layout>
</route>
<route id="ideation" reference="references/ideation-diagrams.md">
<when>brainstorm, notes, cluster, opportunity map, organize ideas</when>
<goal>surface patterns</goal>
<layout>center-out</layout>
</route>
<route id="ux" reference="references/ux-designer-diagrams.md">
<when>journey, wireflow, sitemap, screen flow, onboarding, checkout</when>
<goal>make the main path obvious</goal>
<layout>left-to-right</layout>
</route>
<route id="sales-funnel" reference="references/sales-funnel-diagrams.md">
<when>funnel, conversion, drop-off, lead stages, qualification</when>
<goal>show narrowing and leak points</goal>
<layout>top-wide to bottom-narrow</layout>
</route>
<route id="technical-explainer" reference="references/technical-explainer-diagrams.md">
<when>architecture, API, protocol, request flow, event flow, integration, retry</when>
<goal>explain one technical question well</goal>
<layout>left-to-right or top-to-bottom</layout>
</route>
<route id="medical" reference="references/medical-diagrams.md">
<when>condition, symptom, diagnosis, treatment, patient explanation</when>
<goal>plain-language medical clarity</goal>
<layout>top-to-bottom or left-to-right</layout>
</route>
<route id="creative-raw" reference="references/creative-raw-diagrams.md">
<when>full creative mode, raw creative mode, open composition, choose any diagram style, do whatever fits best</when>
<goal>use the best hand-drawn composition without forcing a narrow route</goal>
<layout>pattern-driven</layout>
</route>
<route id="page-mockup" reference="references/page-mockup-diagrams.md">
<when>webpage, landing page, dashboard, pricing page, UI mockup</when>
<goal>read like a webpage with strong hierarchy</goal>
<layout>top-to-bottom page sections</layout>
</route>
</routes>
<disambiguation>
<rule>If the page itself is the deliverable, use page-mockup. If the flow is the deliverable, use ux.</rule>
<rule>If the request is technical and educational, use technical-explainer first and borrow teaching simplicity only if needed.</rule>
<rule>If the request is exploratory, use ideation. If it is already structured, use the domain route instead.</rule>
<rule>Use creative-raw only when the user explicitly asks for broad creative freedom. Otherwise prefer a narrower route.</rule>
</disambiguation>
</activation-routing>
Animation Spec
Read this before writing a .animationinfo.json file.
This reference covers everything excalidraw-animate can do and how to make the LLM-authored animation tell a clear story.
---
What animateSvg Does Per Element Type
Each Excalidraw element type animates differently. Know this before assigning durations.
| Element Type | Animation Behaviour | Notes |
|---|---|---|
rectangle | Border draws itself as a polygon, then fill fades in (75% / 25% split) | Fast border + fill looks good at 400–600ms |
diamond | Same as rectangle — polygon draw + fill | |
ellipse | Outline traces the oval, then fill fades in | |
arrow | Shaft draws first (60% of duration), then arrowhead paths animate | Give arrows slightly more time than boxes |
line | Draws along its path from start to end | Fast at 200–300ms |
text | Reveals left-to-right along a sliding path clip | Short text: 200ms. Long labels: 400ms |
freedraw | Interpolates stroke points one by one | |
image | Opacity fade-in only — no path animation possible | 300ms fade is enough |
---
Duration Rules
Global defaults (when no duration specified)
- Individual ungrouped element:
500ms - Grouped elements total:
5000msdivided equally among group members
Recommended ranges
- Short label / title text:
200–300ms - Box or ellipse:
400–600ms - Arrow:
400–700ms - Section header:
300ms - Complex grouped region:
800–1200mstotal
Total animation time
- Aim for the full diagram to finish in 8–20 seconds for most diagrams.
- For dense diagrams (15+ elements): use
defaultDuration: 300and group related elements. - For simple diagrams (5–8 elements): use
defaultDuration: 500.
---
Order Rules
order controls when an element animates relative to others.
- Elements with the same
ordervalue animate simultaneously. - Elements with lower
orderanimate first. order: 1is the first thing to appear.- Elements not listed in
elements[]animate in their array creation order after all listed elements.
Key ordering principles
- Titles and headings animate first (order 1).
- Containers before their content — the box draws before the text inside.
- Arrows after their source and target — never animate an arrow before the boxes it connects.
- Group related elements at the same order — a container and its bound text should share an order so they appear together.
- Don't give every element a unique order unless you want strictly sequential (adds time). Siblings at the same level should share an order.
---
animationinfo.json Format
{
"startMs": 500,
"defaultDuration": 500,
"elements": [
{ "id": "elem-id-1", "order": 1, "duration": 300 },
{ "id": "elem-id-2", "order": 1, "duration": 300 },
{ "id": "elem-id-3", "order": 2, "duration": 500 },
{ "id": "arrow-1", "order": 3, "duration": 400 }
]
}| Field | Default | Description |
|---|---|---|
startMs | 500 | Pause before animation begins (ms) |
defaultDuration | 500 | Duration for any element not listed in elements |
elements[].id | required | The exact element id from the .excalidraw file |
elements[].order | 0 | Sequence position — lower = earlier |
elements[].duration | defaultDuration | How long this element takes to draw (ms) |
Use an array, not an object map. The array narrates the story in sequence — the LLM writes it top-to-bottom in the order the story should unfold.
---
Story Patterns
Pick one pattern and apply it to the full diagram. Do not mix patterns within the same diagram.
reveal-top-down
Best for: flows, sequences, step-by-step processes.
Assign order: 1 to the topmost row, order: 2 to the next, and so on. Containers and their bound text share the same order. Arrows between rows get the order of the row they point to.
title order: 1
step-1-box order: 2
step-1-text order: 2
arrow-1→2 order: 3
step-2-box order: 3
step-2-text order: 3
arrow-2→3 order: 4
step-3-box order: 4
step-3-text order: 4context-first
Best for: architecture diagrams, system maps, infrastructure diagrams.
Outer frame or context boundary first, then inner services, then arrows connecting them.
outer-frame order: 1
service-a-box order: 2
service-a-text order: 2
service-b-box order: 2
service-b-text order: 2
arrow-a→b order: 3
arrow-b→c order: 3problem-solution
Best for: teaching diagrams, before/after, explainers.
The problem or question appears first. The solution or answer builds afterward.
question-title order: 1
problem-box order: 2
arrow-to-solution order: 3
solution-box order: 4
solution-text order: 4
takeaway order: 5build-left-right
Best for: pipeline diagrams, data flow, assembly lines.
Each stage (box + label) animates together, sweeping left to right. Arrows between stages animate with the stage they connect to.
stage-1-box order: 1
stage-1-label order: 1
arrow-1→2 order: 2
stage-2-box order: 2
stage-2-label order: 2
arrow-2→3 order: 3
stage-3-box order: 3
stage-3-label order: 3simultaneous-groups
Best for: side-by-side comparisons, two-column layouts.
Both columns animate in parallel row by row — same order values on left and right.
left-title order: 1
right-title order: 1
left-row1 order: 2
right-row1 order: 2
left-row2 order: 3
right-row2 order: 3
divider-line order: 1---
What Not To Do
- Don't set order:0 on everything — that degenerates to default array order with no story.
- Don't give 20 elements 20 unique orders — a 20-element diagram at 500ms each = 10+ seconds of waiting.
- Don't animate arrows before their connected boxes — it looks broken.
- Don't separate a container from its bound text — they should share the same order.
- Don't use durations under 150ms — too fast to perceive the draw effect.
- Don't use durations over 2000ms per element — becomes painfully slow.
---
Writing animationinfo.json — Checklist
Before writing the file: 1. Choose one story pattern that fits the diagram layout. 2. Only list elements that need a specific order or non-default duration. Unlisted elements animate in source-array order at defaultDuration — do not enumerate every element. 3. Group container + bound text pairs at the same order. 4. Put arrows after the shapes they connect. 5. Choose defaultDuration based on diagram density (300 for dense, 500 for normal). 6. Check total time: number of sequential order groups × avg duration should be 8–20s. 7. Write the array in story order — top of array = first to appear.
Arrow Routing
Use this file whenever arrows are doing too much damage to the readability of a diagram.
Core Rule
Arrows may cross other arrows if needed.
Arrows should not cut through boxes, major labels, or evidence panels unless there is no cleaner route.
Preferred Behavior
- arrows leave a shape cleanly from an edge
- arrows travel through whitespace first
- arrows bend or curve around boxes
- arrows land on the nearest sensible side of the target
- arrow crossings are acceptable only when arrow-on-arrow
Routing Priority
1. Do not cut through containers 2. Do not cut through important text 3. Prefer one or two bends over a long straight line through content 4. If a crossing is unavoidable, cross another arrow, not a box
Best Default Styles
Straight Arrow
Use when:
- source and target are aligned
- the path stays in whitespace
Bent Arrow
Use when:
- a straight path would cut a box
- the arrow needs to go around a region
Technique:
- use 3+ points in
arrow.points - keep bends broad and readable
Hand-Drawn Curve
Use when:
- the route should feel organic
- the diagram is dense and a rigid elbow looks too mechanical
Technique:
- use 3+ points
- allow soft detours around the container
- keep the curve simple enough to follow in one glance
Anchor Guidance
- left-to-right flows: leave from the right edge of source, land on left edge of target
- top-to-bottom flows: leave from bottom edge of source, land on top edge of target
- if the nearest edge is blocked, use the next clean edge instead
Never Do This
- one long diagonal arrow slicing through multiple boxes
- arrows cutting through the middle of a large container
- arrows running over dense paragraph text
- forcing perfect straightness when a bend would be clearer
Good Simplification Tricks
- move the label, not the arrow, if the arrow path is already clean
- use a flow spine plus short local arrows instead of one giant connector
- split the diagram into panels if routing becomes impossible
- shorten arrows by moving related elements closer
Bad vs Better
Bad: Service A -----------------------> Service B through the middle of 2 containers
Better: Arrow exits Service A, bends through whitespace, then lands on the edge of Service B.
Acceptance Check
- Do arrows avoid cutting through boxes?
- If arrows cross, are they mostly crossing other arrows?
- Can you trace each arrow path quickly?
- Would one bend or curve make the route cleaner?
Bullet Lists (Hand-Drawn Style)
Keep bullets readable, short, and aligned. Use the same sketch font (fontFamily: 1) and avoid cramming paragraphs into one list.
How to Build
- Use a single
textelement with line breaks for the list when possible. - Keep
autoResize: true. - Left align (
textAlign: "left"),lineHeight: 1.25. - Use a simple glyph:
•or-(preferred:•). - Limit line length so it does not wrap awkwardly; expand the container instead of squeezing text.
- One list per container; avoid scattering bullets across multiple boxes.
Spacing
- One blank space after the bullet glyph.
- Consistent indentation for wrapped lines (visually align with the text start).
- Vertical spacing: rely on line height; do not add manual blank lines unless separating sections.
When to Use a Container
- Use a container only if the bullets represent one panel/section.
- If using a container:
- bind the text with
containerId - include the text id in the container
boundElements - size the container so no bullets clip or overflow
Keep It Short
- Prefer 3–7 bullets per list.
- Each bullet should carry one idea.
- If a bullet is long, split into two bullets or a short sub-line (indented) rather than a long sentence.
Do Not
- Do not mix multiple bullet styles in one list.
- Do not center bullets.
- Do not leave bullets half-wrapped outside the box; grow the box instead.
- Do not repeat long text across multiple bullet lists.
Color Palette & Brand Style
This skill uses a simple hand-drawn aesthetic with black strokes only. All elements use consistent black stroke with white/transparent fills for a clean, sketch-like appearance.
---
Default Colors (Always Use These)
All shapes and elements:
- Stroke color:
#1e1e1e(black) - Fill color:
#ffffff(white) ortransparent(no fill) - Stroke width:
2(standard) - Roughness:
1(always - for hand-drawn look)
All text:
- Color:
#1e1e1e(black) - Font family:
1(Virgil handwritten)
Arrows:
- Color:
#1e1e1e(black) - Stroke width:
2
Lines (structural):
- Color:
#1e1e1e(black) - Stroke width:
1or2
---
Variations (Only When Needed)
| Purpose | Settings |
|---|---|
| Emphasis | Increase stroke width to 3 or 4 |
| De-emphasis | Use strokeStyle: "dashed" |
| Small markers/dots | Fill and stroke both #1e1e1e, 10-20px size |
---
Background
| Property | Value |
|---|---|
| Canvas background | #ffffff |
---
No other colors are allowed. Keep the hand-drawn, sketch-like aesthetic simple and consistent.
Evidence artifacts follow the same rule: code snippets, JSON panels, and mockups must remain monochrome with black text, black lines, and white or transparent backgrounds only.
Creative Raw Diagrams
Use this when the user explicitly asks for full creative freedom, raw composition, or says to choose whatever diagram style fits best.
Output should still be hand-drawn, readable, and coherent, but it does not need to follow one narrow domain route.
Shape vocabulary: see references/fundamental-shapes.md.
One Big Idea
Even in raw mode, one diagram should still answer one main idea.
Best Default Layout
Pick the best pattern for the content instead of forcing a route:
Hero + SatellitesAnnotated ObjectAssembly lineTimelineSide-by-SideTree
Must Show
- 1 clear visual anchor
- 1 obvious reading path
- only meaningful elements
Never Draw
- empty placeholder boxes
- decorative containers with no content
- repeated text blocks saying nearly the same thing
- generic equal-card layouts with no hierarchy
Hard Limits
- major sections: 3-7
- empty containers: 0
- duplicated labels: 0
Acceptance Check
- Does it still feel hand-drawn and coherent?
- Does the freedom improve the explanation instead of making it messy?
- Are all visible containers doing real work?
Element Templates
Copy-paste JSON templates for each Excalidraw element type. All elements use black stroke (#1e1e1e) with white/transparent fills for a simple hand-drawn aesthetic.
These are safe hand-authored starting points, not a full dump of every property Excalidraw may serialize.
Canvas Layout Grid
Canvas: 1400 × 900px. Pick a unique cell per top-level element. Do not compute — read x,y directly from this table:
col1 col2 col3 col4 col5
row1 80,80 360,80 640,80 920,80 1200,80
row2 80,200 360,200 640,200 920,200 1200,200
row3 80,320 360,320 640,320 920,320 1200,320
row4 80,440 360,440 640,440 920,440 1200,440
row5 80,560 360,560 640,560 920,560 1200,560
row6 80,680 360,680 640,680 920,680 1200,680
row7 80,800 360,800 640,800 920,800 1200,800Arrow x,y: start at source.x + source.width + 2, end at target.x - 2. Vertical: source.y + source.height + 2.
Rules:
- Bound text inherits its container's cell — do not assign it a separate cell.
- If you need more than 35 cells, shrink element sizes and shift values accordingly.
- Seeds: use distinct integers. Simple strategy: first element seed=10001, increment by 1 per element.
Free-Floating Text (no container)
{
"type": "text",
"id": "label1",
"x": 100, "y": 100,
"width": 200, "height": 25,
"text": "Section Title",
"originalText": "Section Title",
"fontSize": 20,
"fontFamily": 1,
"textAlign": "left",
"verticalAlign": "top",
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"seed": 11111,
"version": 1,
"versionNonce": 22222,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"containerId": null,
"autoResize": true,
"lineHeight": 1.25
}Line (structural, not arrow)
{
"type": "line",
"id": "line1",
"x": 100, "y": 100,
"width": 0, "height": 200,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"seed": 44444,
"version": 1,
"versionNonce": 55555,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"points": [[0, 0], [0, 200]]
}Small Marker Dot
{
"type": "ellipse",
"id": "dot1",
"x": 94, "y": 94,
"width": 12, "height": 12,
"strokeColor": "#1e1e1e",
"backgroundColor": "#1e1e1e",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"seed": 66666,
"version": 1,
"versionNonce": 77777,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false
}Rectangle
{
"type": "rectangle",
"id": "elem1",
"x": 100, "y": 100, "width": 180, "height": 90,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"seed": 12345,
"version": 1,
"versionNonce": 67890,
"isDeleted": false,
"groupIds": [],
"boundElements": [{"id": "text1", "type": "text"}],
"link": null,
"locked": false,
"roundness": {"type": 3}
}Text (centered in shape)
{
"type": "text",
"id": "text1",
"x": 130, "y": 132,
"width": 120, "height": 25,
"text": "Process",
"originalText": "Process",
"fontSize": 16,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "middle",
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"seed": 11111,
"version": 1,
"versionNonce": 22222,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"containerId": "elem1",
"autoResize": true,
"lineHeight": 1.25
}Arrow
{
"type": "arrow",
"id": "arrow1",
"x": 282, "y": 145, "width": 118, "height": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"seed": 33333,
"version": 1,
"versionNonce": 44444,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"points": [[0, 0], [118, 0]],
"startBinding": {"elementId": "elem1", "focus": 0, "gap": 2},
"endBinding": {"elementId": "elem2", "focus": 0, "gap": 2},
"startArrowhead": null,
"endArrowhead": "arrow"
}Use this only when the path stays mostly in whitespace.
Arrow routing notes:
- Prefer edge-to-edge bindings, not center-to-center diagonals.
- If a straight path would cut through a box, switch to a bent or curved arrow.
- Arrow-on-arrow crossings are acceptable. Arrow-through-box routing is not a good default.
Bent Arrow Around A Container
{
"type": "arrow",
"id": "arrowBent1",
"x": 282, "y": 145, "width": 198, "height": 120,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"seed": 33335,
"version": 1,
"versionNonce": 44446,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"points": [[0, 0], [60, 0], [60, 90], [198, 120]],
"startBinding": {"elementId": "elem1", "focus": 0.15, "gap": 2},
"endBinding": {"elementId": "elem2", "focus": -0.2, "gap": 2},
"startArrowhead": null,
"endArrowhead": "arrow"
}Use this when the direct line would run through an intermediate box or dense label area.
Curved Arrow
{
"type": "arrow",
"id": "arrowCurve1",
"x": 280, "y": 140, "width": 180, "height": 80,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"seed": 33334,
"version": 1,
"versionNonce": 44445,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"points": [[0, 0], [90, -40], [180, 40]],
"startBinding": {"elementId": "elem1", "focus": 0, "gap": 2},
"endBinding": {"elementId": "elem2", "focus": 0, "gap": 2},
"startArrowhead": null,
"endArrowhead": "arrow"
}For curves: use 3+ points in points array and keep the detour broad enough to read in one glance.
Frame (section grouping only)
{
"type": "frame",
"id": "frame1",
"x": 60, "y": 60, "width": 520, "height": 280,
"strokeColor": "#bbbbbb",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"angle": 0,
"seed": 88888,
"version": 1,
"versionNonce": 99999,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"name": "Section",
"frameId": null
}Section Title + Divider
[
{
"type": "text",
"id": "sectionTitle1",
"x": 80, "y": 70,
"width": 220, "height": 25,
"text": "Section Title",
"originalText": "Section Title",
"fontSize": 20,
"fontFamily": 1,
"textAlign": "left",
"verticalAlign": "top",
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"seed": 10001,
"version": 1,
"versionNonce": 10002,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"containerId": null,
"lineHeight": 1.25
},
{
"type": "line",
"id": "sectionDivider1",
"x": 80, "y": 104,
"width": 220, "height": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"angle": 0,
"seed": 10003,
"version": 1,
"versionNonce": 10004,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"points": [[0, 0], [220, 0]]
}
]Bound Container Pair
[
{
"type": "rectangle",
"id": "container1",
"x": 100, "y": 100, "width": 220, "height": 100,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"seed": 12346,
"version": 1,
"versionNonce": 67891,
"isDeleted": false,
"groupIds": [],
"boundElements": [{"id": "containerText1", "type": "text"}],
"link": null,
"locked": false,
"roundness": {"type": 3}
},
{
"type": "text",
"id": "containerText1",
"x": 210, "y": 150,
"width": 168, "height": 44,
"text": "Bound text block",
"originalText": "Bound text block",
"fontSize": 16,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "middle",
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"seed": 11112,
"version": 1,
"versionNonce": 22223,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"containerId": "container1",
"autoResize": true,
"lineHeight": 1.25
}
]Notes:
- Keep
originalTextas the clean source string. - Keep
autoResize: truefor bound text. - Treat the text
width,height,x, andyas starter values only; Excalidraw may recompute them when rendered or edited.
Multi-Line Text Block
{
"type": "text",
"id": "textMulti1",
"x": 100, "y": 100,
"width": 240, "height": 72,
"text": "Line one\nLine two\nLine three",
"originalText": "Line one\nLine two\nLine three",
"fontSize": 16,
"fontFamily": 1,
"textAlign": "left",
"verticalAlign": "top",
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"seed": 12121,
"version": 1,
"versionNonce": 23232,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"containerId": null,
"autoResize": true,
"lineHeight": 1.25
}Arrow With Label
[
{
"type": "arrow",
"id": "arrowLabel1",
"x": 280, "y": 140, "width": 180, "height": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"seed": 45551,
"version": 1,
"versionNonce": 45552,
"isDeleted": false,
"groupIds": [],
"boundElements": [{"id": "arrowLabelText1", "type": "text"}],
"link": null,
"locked": false,
"points": [[0, 0], [180, 0]],
"startBinding": {"elementId": "elem1", "fixedPoint": [1, 0.5], "mode": "inside"},
"endBinding": {"elementId": "elem2", "fixedPoint": [0, 0.5], "mode": "inside"},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": false
},
{
"type": "text",
"id": "arrowLabelText1",
"x": 340, "y": 122,
"width": 70, "height": 20,
"text": "Label",
"originalText": "Label",
"fontSize": 16,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "middle",
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"seed": 45553,
"version": 1,
"versionNonce": 45554,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"containerId": "arrowLabel1",
"autoResize": true,
"lineHeight": 1.25
}
]Arrow label notes:
- The label is bound text on the arrow via
containerId. - The arrow must include the text element in
boundElements. - Keep
autoResize: true. - If the label makes a clean route messy, move or shorten the label before forcing the arrow through a box.
- Do not hand-position long arrow labels too tightly. Excalidraw calculates final placement from the arrow path.
Fundamental Shapes
Use this file as the shared visual grammar for all domain guides.
These shapes already exist in standard Excalidraw and should be enough for most hand-drawn diagrams.
Core Rule
Prefer a small repeated vocabulary of shapes over inventing a new shape language for every domain.
Standard Shape Meanings
| Excalidraw Type | Default Meaning | Use Notes |
|---|---|---|
text | Label, explanation, annotation | Default choice for supporting detail |
rectangle | Process, step, component, screen, stage | Best general-purpose container |
ellipse | Input, output, actor, state, external force | Use when something feels softer or origin-like |
diamond | Decision, branch, conditional, diagnostic gate | Use only when a true choice exists |
arrow | Directed movement, causality, transition, handoff | Bind at both ends when possible; route around containers when needed |
line | Divider, timeline spine, grouping structure | Use when relation is structural, not directional |
frame | Section boundary, region, panel | Use for big grouping only, not every box |
Hand-Drawn Defaults
strokeColor: #1e1e1ebackgroundColor: #ffffffortransparentroughness: 1opacity: 100fontFamily: 1
Icon Rule
Use small hand-drawn icon cues when they improve understanding.
Examples:
- sun, eye, bulb, cloud, lock, database cylinder, phone, browser window
- tiny status marks like check, cross, spark, warning burst
These should feel like sketch doodles or emoji-like cues, not polished product icons.
Use them to:
- make a hero concept memorable
- distinguish categories quickly
- reduce repeated text labels
Do not:
- replace the whole diagram with icons
- mix multiple icon styles
- use colorful polished icon packs in monochrome modes
Domain Mapping
Teaching
ellipse: topic or questionrectangle: explanation stepline: timeline or divider
Ideation
ellipse: cluster or themerectangle: selected idea or next actionframe: optional region for major groups
UX
rectangle: screen or statediamond: decision pointarrow: user movement
Funnel
ellipse: source or audiencerectangle: stagediamond: qualification gate
Technical / Architecture / API / Protocol
ellipse: external actor, trigger, inbound/outbound eventrectangle: service, process, system boundary, data transformationdiamond: routing or validation choiceline: timeline spine or sequence backbone
Medical
ellipse: patient, symptom, body state, outcomerectangle: intervention, step, treatment, assessmentdiamond: diagnostic or triage decisionarrow: progression, escalation, cause/effect
Never Do This
- Assign a new meaning to the same shape inside one diagram
- Use diamonds for things that are not real decisions
- Use frames as decorative cards
- Put paragraphs inside rectangles when text could float outside
- Run long arrows through boxes when a bend or curve would be clearer
- Turn every idea into the same rounded box when a timeline, doodle, comparison, or floating label would teach better
Minimum Useful Vocabulary
If the diagram gets too busy, reduce back to:
rectangleellipsearrowtext
That four-part vocabulary is enough for most diagrams.
Ideation Diagrams
Use this when the input is messy and the goal is to surface patterns, options, or priorities.
Output should look like one central question, a few visible clusters, and one promising direction.
Shape vocabulary: see references/fundamental-shapes.md.
One Big Idea
The diagram should answer one question only: what are we trying to explore?
Best Default Layout
Default flow: center-out
[Cluster]
\
[Cluster]-[Question]-[Cluster]
/
[Next step]Reveal order: 1. The central question 2. The major clusters 3. The strongest opportunity 4. The next step
Must Show
- 1 central prompt
- 3-5 clusters max
- 1 highlighted opportunity or next-step region
- visible difference between raw ideas and selected ideas
Never Draw
- perfectly symmetrical idea boards
- arrows between every note
- finished-looking strategy maps when the thinking is still early
Canonical Recipes
1. Brain dump to clusters
Structure: Central question -> 4 clusters -> highlighted opportunity
2. Options exploration
Structure: Prompt -> 3 paths -> one tradeoff note per path -> provisional best option
3. Decision framing
Structure: Question -> choices -> risks/benefits -> next-step decision
Hard Limits
- Clusters: 3-5
- Raw notes per cluster: 3 max
- Highlighted bets: 1-2
- Cross-cluster arrows: 3 max
Bad vs Better Prompt
Bad: "Turn these notes into an ideation diagram."
Better: "Turn these notes into a hand-drawn ideation map with 4 clusters, 1 highlighted opportunity, and no more than 3 notes per cluster."
Cut Ruthlessly
Remove in this order: 1. Duplicate ideas 2. weak relationship arrows 3. extra labels explaining obvious clusters
Minimum Viable Output
- 1 central question
- 3 clusters
- 1 highlighted opportunity
- 1 next step
Acceptance Check
- Is the central question unmistakable?
- Can you see clusters before reading the text?
- Is one promising direction clearly favored?
- Does the diagram still feel exploratory rather than falsely final?
References Index
Load only what the current task needs.
Start Here
activation-routing.xml: choose one routefundamental-shapes.md: shared visual grammar
Route Guides
route-guide-template.mdteachers-diagrams.mdideation-diagrams.mdux-designer-diagrams.mdsales-funnel-diagrams.mdtechnical-explainer-diagrams.mdmedical-diagrams.mdcreative-raw-diagrams.mdpage-mockup-diagrams.md
Drawing Support
patterns.mdprompt-templates.mdelement-templates.mdjson-schema.mdbullets.md
Quality
arrow-routing.mdquality-checklist.mdcolor-palette.md
Animation
animation-spec.md: LLM reference for writing.animationinfo.json— per-element behaviour, duration rules, story patterns
Excalidraw JSON Schema
This reference focuses on the subset most useful for this skill while noting the modern fields you may see in current Excalidraw files.
File Wrapper
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [],
"appState": {},
"files": {}
}| Property | Description |
|---|---|
type | Always excalidraw |
version | File format version |
source | Usually https://excalidraw.com |
elements | Array of scene elements |
appState | Export/view settings such as background color and grid |
files | Binary file map used for image-backed elements |
Element Types
| Type | Use For |
|---|---|
rectangle | Processes, actions, components |
ellipse | Entry/exit points, external systems |
diamond | Decisions, conditionals |
arrow | Connections between shapes |
text | Labels inside shapes |
line | Non-arrow connections |
frame | Grouping containers |
The broader Excalidraw schema also includes types such as freedraw, image, embeddable, iframe, and magicframe. This skill normally avoids those unless the user explicitly needs them.
Common Properties
All elements share these:
| Property | Type | Description |
|---|---|---|
id | string | Unique identifier |
type | string | Element type |
x, y | number | Position in pixels |
width, height | number | Size in pixels |
strokeColor | string | Border color (hex) |
backgroundColor | string | Fill color (hex or "transparent") |
fillStyle | string | "solid", "hachure", "cross-hatch" |
strokeWidth | number | 1, 2, or 4 |
strokeStyle | string | "solid", "dashed", "dotted" |
roughness | number | 0 (smooth), 1 (default), 2 (rough) |
opacity | number | 0-100 |
seed | number | Random seed for roughness |
version | number | Incremented on edit |
versionNonce | number | Additional reconciliation nonce |
index | string or null | Fractional ordering metadata |
groupIds | array | Nested group membership |
frameId | string or null | Parent frame, if grouped in a frame |
boundElements | array or null | Bound text/arrows metadata |
updated | number | Last updated epoch ms |
link | string or null | Optional hyperlink |
locked | boolean | Whether the element is locked |
isDeleted | boolean | Soft deletion flag |
customData | object | Optional app-specific metadata |
Text-Specific Properties
| Property | Description |
|---|---|
text | The display text |
originalText | Same as text |
fontSize | Size in pixels (16-20 recommended) |
fontFamily | 1 for Virgil handwritten (use this) |
textAlign | "left", "center", "right" |
verticalAlign | "top", "middle", "bottom" |
containerId | ID of parent shape |
lineHeight | Unitless line height |
autoResize | Whether text resizes automatically |
For hand-authored diagrams:
originalTextshould preserve the clean source string.textmay be wrapped by Excalidraw for bound/container text.- Bound text inside shapes and arrows usually works best with
autoResize: true. - Fixed
width/heightguesses are a common source of clipped text.
Arrow-Specific Properties
| Property | Description |
|---|---|
points | Array of [x, y] coordinates |
startBinding | Connection to start shape |
endBinding | Connection to end shape |
startArrowhead | null or an arrowhead type |
endArrowhead | null or an arrowhead type |
elbowed | Optional orthogonal routing mode |
Common arrowheads in current Excalidraw include arrow, bar, dot, triangle, triangle_outline, diamond, diamond_outline, circle, and circle_outline.
Arrow labels are represented as text elements whose containerId points to the arrow id, while the arrow lists the text in boundElements.
Binding Format
{
"elementId": "shapeId",
"focus": 0,
"gap": 2
}Modern Export Notes
- Current Excalidraw utility APIs expose both
exportToSvg()andexportToBlob(). - Scene size is effectively unbounded for authoring, but PNG/canvas export can still hit browser raster limits.
- For very large diagrams, SVG or scaled raster export is safer than assuming one huge PNG will always work.
Rectangle Roundness
Add for rounded corners:
"roundness": { "type": 3 }Medical Diagrams
Use this when the goal is to explain symptoms, conditions, body processes, clinical pathways, triage, treatment flow, or patient education.
Output should look like one clear pathway, one patient-relevant explanation, and one visible outcome or decision point.
Shape vocabulary: see references/fundamental-shapes.md.
One Big Idea
One diagram should explain one medical question only.
Examples:
- What is happening in the body
- How symptoms progress
- How diagnosis or triage works
- What treatment sequence looks like
Best Default Layout
Default flow: top-to-bottom for clinical pathways, left-to-right for body-process explanation
Clinical pathway skeleton:
[Patient/Symptom]
|
[Assessment]
|
[Decision]
/ \
[Path A] [Path B]Body-process skeleton:
[Cause] -> [Body process] -> [Symptom] -> [Action/Outcome]Reveal order: 1. Patient state, symptom, or condition 2. What is happening 3. The key decision or intervention 4. The outcome or next action
Must Show
- 1 medical question only
- 3-5 major states or steps
- 1 visible decision point if diagnosis or triage is involved
- 1 patient-facing outcome or action
- plain-language labels unless expert audience is explicit
Never Draw
- dense textbook-style anatomy boards
- 8 competing symptom branches
- jargon-heavy labels with no patient meaning
Canonical Recipes
1. Symptom to action
Structure: Symptom -> likely cause -> what to do
2. Body-process explanation
Structure: Cause -> internal process -> symptom -> result
3. Triage or diagnosis path
Structure: Patient state -> assessment -> decision -> next path
4. Treatment flow
Structure: Condition -> intervention steps -> expected outcome
Use comparison when possible. Normal vs affected, mild vs severe, and before treatment vs after treatment are often simpler than branching webs.
Fundamental Shape Mapping
ellipse: patient, symptom, state, outcomerectangle: assessment, treatment, intervention, explanatory stepdiamond: diagnostic gate, urgency decision, escalation choicearrow: progression, cause/effect, clinical handoffframe: optional section for symptoms, diagnosis, treatment, recovery
Hard Limits
- Major steps or states: 3-5
- Decision diamonds: 2 max
- body regions or sub-processes: 3 max
- annotations: 4 max
Language Rules
- Prefer everyday words first
- Add medical terms only if needed
- Keep labels short
- Use one safety-oriented action if appropriate
Bad vs Better Prompt
Bad: "Make a medical hand-drawn diagram about asthma."
Better: "Create a hand-drawn medical explainer diagram that shows what happens during an asthma attack using 4 steps, 1 plain-language body-process explanation, and 1 action-oriented takeaway."
Cut Ruthlessly
Remove in this order: 1. secondary anatomy detail 2. low-value labels 3. extra branches that do not change the main explanation
Minimum Viable Output
- 1 symptom or condition
- 3-step process
- 1 decision or action
- 1 outcome or takeaway
Acceptance Check
- Is the main medical story understandable in one pass?
- Does the diagram avoid unnecessary jargon?
- Is the key decision or action visible?
- Would the intended audience know what to do or understand after seeing it?
Page Mockup Diagrams
Use this when the user wants a hand-drawn page mockup, landing page sketch, dashboard sketch, or webpage-like composition rather than a pure systems diagram.
Output should look like a rough webpage, not a flowchart: visible sections, UI hierarchy, and realistic component placement.
Shape vocabulary: start from references/fundamental-shapes.md, then adapt those shapes into UI blocks.
Special Rule
Default skill behavior is monochrome.
For page mockup diagrams only, color is allowed when the user explicitly wants webpage-like fidelity, stronger UX communication, or visual emphasis.
Keep these constraints even when using color:
- same hand-drawn font family
- same rough/sketch feeling
- limited palette
- color supports hierarchy, not decoration
Wireframe note:
- If the user says
wireframe, default to monochrome or near-monochrome first. - If the user says
page mockup,landing page, or explicitly asks for richer webpage fidelity, restrained color is allowed.
One Big Idea
One mockup diagram should explain one page only.
Examples:
- landing page
- dashboard home
- pricing page
- onboarding page
- checkout page
Best Default Layout
Default flow: top-to-bottom page structure
[Header / Nav]
[Hero]
[Primary content]
[Secondary section]
[CTA / Footer]Reveal order: 1. Hero section 2. Primary action 3. Supporting sections 4. Footer or closing CTA
Must Show
- one dominant page purpose
- one hero section
- one primary CTA
- 3-6 page sections max
- clear hierarchy between headline, content, and action
Never Draw
- every section with the same size and weight
- too many small labels floating everywhere
- generic wireframe boxes with no page rhythm
Fundamental Shape Mapping For UI
rectangle: card, hero block, panel, section, form areaellipse: badge, avatar, icon spot, accent chipline: dividers, navbar separators, chart axes, layout rhythmdiamond: use rarely, only if a page includes explicit decision logicarrow: only for interaction explanation or page flow overlaysframe: use for major page zones or alternative screen statestext: headings, labels, short content, CTA text
Color Rules
Use color only if explicitly requested.
When color is used:
- keep one base background
- one primary accent color
- one secondary support color max
- keep text readable
- avoid rainbow UI
Recommended page-mockup palette pattern:
- Base background: white or very light neutral
- Primary text: near-black
- Primary accent: one saturated button/action color
- Secondary accent: one support color for chips, charts, or active states
Recommended wireframe palette pattern:
- White background
- Near-black text and strokes
- Light gray surfaces
- Optional one muted accent for CTA or active tab only
Good uses of color:
- CTA emphasis
- active nav state
- chart bars or stats
- hero highlight
- status chips
- selected plan card
- active step in onboarding or checkout
Bad uses of color:
- every card different
- multicolor text
- decorative gradients with no communicative value
- full marketing-art direction inside a rough wireframe
Canonical Recipes
1. Landing page mockup
Structure: Header -> Hero -> Proof -> Features -> CTA -> Footer
2. Dashboard mockup
Structure: Top bar -> KPI row -> main chart/content -> side panel or recent activity
3. Pricing page mockup
Structure: Header -> Pricing hero -> plan cards -> FAQ -> CTA
4. Checkout or signup page
Structure: Header -> focused form -> reassurance/supporting detail -> submit action
5. Wireframe-style page
Structure: Header -> hero/content block -> supporting block -> CTA, using mostly monochrome with optional one muted accent
Hard Limits
- Sections: 3-6
- Primary CTAs: 1-2
- Accent colors: 1-2
- Major cards/panels in one viewport: 6 max
- Annotation arrows: 3 max
- Distinct button styles: 2 max
UX Quality Rules
- one obvious primary action
- one strongest visual region
- supporting sections should reduce doubt, not compete with the hero
- spacing should communicate importance
- if comparison helps, use before/after or plan-vs-plan instead of extra explanation text
Bad vs Better Prompt
Bad: "Make a website mockup."
Better: "Create a hand-drawn landing page mockup for a SaaS product with the same sketch font, one hero CTA, 5 sections, and limited accent color for the button and highlights."
Wireframe version: "Create a hand-drawn wireframe for a pricing page using the same sketch font, 4 sections, monochrome layout blocks, and one muted accent for the main CTA."
Cut Ruthlessly
Remove in this order: 1. extra sections 2. secondary buttons 3. decorative accents that do not improve hierarchy
Minimum Viable Output
- header
- hero
- one supporting section
- one CTA
Acceptance Check
- Does it read like a webpage before reading every label?
- Is the page purpose obvious in the hero?
- Is there one clear primary action?
- If color is used, does it improve hierarchy instead of decoration?
Visual Pattern Library
Fan-Out (One-to-Many)
Central element with arrows radiating to multiple targets. Use for: sources, PRDs, root causes, central hubs.
○
↗
□ → ○
↘
○Implementation hint: one central ellipse or rectangle, 2-6 target elements, and bound arrow elements radiating outward. Let arrows bow outward or bend if straight lines would cut through nodes.
Convergence (Many-to-One)
Multiple inputs merging through arrows to single output. Use for: aggregation, funnels, synthesis.
○ ↘
○ → □
○ ↗Implementation hint: multiple source nodes plus arrow elements converging into one destination shape. Leave enough whitespace so arrows can merge without slicing through containers.
Tree (Hierarchy)
Parent-child branching with connecting lines and free-floating text (no boxes needed). Use for: file systems, org charts, taxonomies.
label
├── label
│ ├── label
│ └── label
└── labelUse line elements for the trunk and branches, free-floating text for labels. Implementation hint: use line for the trunk/branches and free-floating text, avoiding boxes unless a node truly needs one.
Spiral/Cycle (Continuous Loop)
Elements in sequence with arrow returning to start. Use for: feedback loops, iterative processes, evolution.
□ → □
↑ ↓
□ ← □Implementation hint: use 3+ arrow.points for bends or curves; optional roundness can soften the loop. Prefer soft detours around boxes over straight cuts through them.
Cloud (Abstract State)
Overlapping ellipses with varied sizes. Use for: context, memory, conversations, mental states. Implementation hint: overlap 3-5 ellipse elements with varied scale and minimal labels.
Assembly Line (Transformation)
Input → Process Box → Output with clear before/after. Use for: transformations, processing, conversion.
○○○ → [PROCESS] → □□□
chaos orderImplementation hint: keep the main path linear with one dominant process shape and short supporting labels.
Side-by-Side (Comparison)
Two parallel structures with visual contrast. Use for: before/after, options, trade-offs. Implementation hint: mirror layout left/right and use one divider line or whitespace gap between the sides.
Gap/Break (Separation)
Visual whitespace or barrier between sections. Use for: phase changes, context resets, boundaries. Implementation hint: use whitespace first, then optional dashed line or a light frame if grouping clarity is needed.
Hero + Satellites
One central visual idea with a few supporting callouts. Use for: teaching, explainers, concept overviews.
[hero doodle]
/ | \
note note noteImplementation hint: use one memorable center element such as an eye, sun, API box, or browser sketch, then 3-4 short supporting notes around it.
Annotated Object
One sketched object with labels around it. Use for: optics, anatomy, page explanation, component breakdown.
label -> [object sketch] <- label
^ |
label labelImplementation hint: draw one main object with simple lines/shapes, keep labels outside the object, and use short arrows only where needed.
Lines as Structure
Use lines (type: line, not arrows) as primary structural elements instead of boxes:
- Timelines: Vertical or horizontal line with small dots (10-20px ellipses) at intervals, free-floating labels beside each dot
- Tree structures: Vertical trunk line + horizontal branch lines, with free-floating text labels (no boxes needed)
- Dividers: Thin dashed lines to separate sections
- Flow spines: A central line that elements relate to, rather than connecting boxes
Timeline: Tree:
●─── Label 1 │
│ ├── item
●─── Label 2 │ ├── sub
│ │ └── sub
●─── Label 3 └── itemLines + free-floating text often creates a cleaner result than boxes + contained text.
Arrow Routing Rule
If a straight arrow would cut through a box, bend or curve it instead.
Arrow-on-arrow crossings are acceptable. Arrow-through-box routing is usually not.
Concept-to-Pattern Mapping
| If the concept... | Use this pattern |
|---|---|
| Spawns multiple outputs | Fan-out (radial arrows from center) |
| Combines inputs into one | Convergence (funnel, arrows merging) |
| Has hierarchy/nesting | Tree (lines + free-floating text) |
| Is a sequence of steps | Timeline (line + dots + free-floating labels) |
| Loops or improves continuously | Spiral/Cycle (arrow returning to start) |
| Is an abstract state or context | Cloud (overlapping ellipses) |
| Transforms input to output | Assembly line (before → process → after) |
| Compares two things | Side-by-side (parallel with contrast) |
| Separates into phases | Gap/Break (visual separation between sections) |
| Needs one memorable concept with support | Hero + Satellites (central doodle + notes) |
| Is easiest to explain as one thing with labels | Annotated Object (object + callouts) |
Prompt Templates
Use these prompt formulas after selecting a mode from references/activation-routing.xml.
The node counts in brackets are starting guidance, not hard caps. Use as many nodes as the task requires to be complete. Every node must earn its place, but earning a place means being part of the answer — not being rare.
Global Formula
Create a hand-drawn [diagram type] for [audience/use case] that explains [topic].
Cover all key concepts needed to fully answer the question.
Use [layout direction] and pick the best visual pattern for the subject.
Keep each node distinct — no duplicate labels or decorative filler.
Route arrows through whitespace; bend or curve around boxes rather than through them.Teachers
Create a hand-drawn teaching diagram that explains [topic] for [audience level].
Cover all major concepts needed to understand [topic] fully.
Use a [top-to-bottom / annotated-object / comparison / tree] layout.
Include 1 visual anchor that makes the topic memorable, 1 concrete example grounded in the real subject, and 1 takeaway.
Avoid turning every concept into the same box — use timelines, trees, or annotated objects where they fit better.
Use the user's language by default; use English only for short standard terms.
Every element must be distinct and necessary.Ideation
Turn these notes into a hand-drawn ideation map.
Use a center-out layout. Cover all the clusters present in the input.
Highlight 1 promising direction clearly.
Keep each note short — bullets or short phrases only.
Make it exploratory, not polished.UX
Create a hand-drawn UX flow for [journey/use case].
Use a left-to-right layout covering all screens or states in the flow.
Include failure or alternate branches where they exist and 1 success outcome.
Keep the main path visually obvious.
Route connectors around screens and boxes — not through them.Sales Funnel
Create a hand-drawn sales funnel for [business/use case].
Use a top-wide to bottom-narrow layout with all funnel stages.
Show major drop-off or friction points and the clear conversion target.
Keep metrics sparse and readable.Technical Explainer
Create a hand-drawn technical explainer diagram for [architecture/API/protocol question].
Use [left-to-right / top-to-bottom] layout covering all primary nodes needed to answer the question.
Include 1 clear flow spine, 1 visual anchor (browser sketch, database cue, etc.), failure branches where they exist, and at least 1 concrete evidence artifact (JSON payload, event name, etc.).
Use real technical names where they matter.
Route arrows around services, databases, and evidence panels; arrow-on-arrow crossings are acceptable.
Do not collapse into a generic row of equal boxes.Medical
Create a hand-drawn medical explainer diagram for [condition/process/question].
Use [top-to-bottom / left-to-right] layout covering all major steps, states, and decision points.
Include all visible decision or action points and 1 patient-facing takeaway.
Prefer plain-language labels unless the audience is expert.Page Mockup
Create a hand-drawn page mockup for [page type/product].
Use a top-to-bottom webpage layout covering all sections of the page.
Keep the sketch font, strong hierarchy, and 1 clear primary CTA.
If color is used, limit to 1-2 accent colors for UX emphasis only.
Only use arrows for annotations or flow overlays; keep them outside major content blocks.Wireframe
Create a hand-drawn wireframe for [page/flow].
Use a [top-to-bottom / left-to-right] layout covering all sections or states.
Keep the sketch font and rough visual style.
Prefer monochrome with minimal accent only if needed for clarity.
Focus on hierarchy, layout, and CTA placement rather than decoration.
Route connectors around screens and boxes — not through them.Comparison Template
Create a hand-drawn comparison diagram for [A] vs [B].
Use a side-by-side layout covering all meaningful comparison points.
Keep the visual weight on the key differences.
Include 1 bottom-line takeaway.Failure / Retry Template
Create a hand-drawn flow diagram for [system/process].
Show the normal path first, then all visible failure branches, then recovery or retry paths.
Mark each decision point clearly.
Prefer a visible spine with short side arrows over one long diagonal connector through multiple boxes.Dense Input Template
Convert this raw input into a hand-drawn diagram:
[paste notes]
Choose the best mode automatically.
Cover all concepts present in the input — do not drop content to reduce node count.
Keep each element distinct and necessary.
Preserve the sketch font and hand-drawn style.Quality Checklist
Run through this before delivering any diagram.
Rule Zero: Completeness
The diagram must fully answer the question the user asked. Check this first.
- Task answered: Does the diagram cover everything the user asked for?
- No missing content: Are all key concepts, steps, or components present?
- No artificial cuts: Were elements removed just to reduce node count rather than because they were unnecessary?
If any of these fail, go back and add the missing content before checking anything else.
---
Validation (Run the script — do not skip)
cd {skill-root}/scripts
uv run python validate_excalidraw.py "/absolute/path/to/file.excalidraw"The script checks:
- Elements array is non-empty
- All IDs are unique
- No
isDeleted: trueelements - All
containerIdreferences resolve - All
boundElementsreferences resolve - All
startBinding/endBindingreferences resolve - No stacked elements (coordinate collision)
- No off-canvas elements
- No text overflow
Fix every error it reports before continuing.
---
Depth & Evidence (For Technical Diagrams)
1. Research done: Looked up actual specs, formats, event names? 2. Evidence artifacts: Code snippets, JSON examples, or real data included where useful? 3. Concrete over abstract: Real content shown, not just labeled boxes? 4. Educational value: Could someone learn something concrete from this?
Conceptual
5. Argument: Does the diagram SHOW something text alone couldn't? 6. Each element distinct: No duplicate labels — same concept not repeated in two boxes 7. No uniform containers: Avoided card grids of equal boxes where a timeline, tree, or annotated object would fit better? 8. Isomorphism: Does each visual structure mirror its concept's behavior?
Container Discipline
9. Minimal containers: Could any boxed element work as free-floating text instead? 10. Lines as structure: Are tree/timeline patterns using lines + text rather than boxes? 11. Typography hierarchy: Are font size and weight creating visual hierarchy (reducing need for boxes)?
Structural
12. Connections: Every relationship has an arrow or line 13. Flow: Clear visual path for the eye to follow 14. Hierarchy: Important elements are larger or more isolated 15. No orphans: No element is disconnected from the rest of the diagram without intent
Technical
16–28. Run `validate_excalidraw.py` — it checks all of these automatically: font family, roughness, unique IDs, binding refs, container refs, text overflow, monochrome style. Fix every reported error before continuing.
Extra checks not covered by the script:
- Frame usage deliberate: Frames group sections, not individual elements
- Bound text uses autoResize: Container text and arrow labels keep
autoResize: true
Layout
29–33. Run `validate_excalidraw.py` — it checks coordinate collisions, off-canvas elements, and spread. Also verify mentally:
- Each top-level element is at a distinct grid cell
- No overcrowded regions; consistent spacing between siblings
Visual Validation (Render when image requested)
34. PNG rendered (if requested): Diagram was rendered and visually inspected when the user asked for an image 35. No text overflow: All text fits within its container 36. No unintended overlaps: Shapes and text don't overlap unless intentionally layered 37. Arrows land correctly: Arrows connect to intended elements without cutting through containers 38. Arrow labels readable: Arrow labels sit on the path cleanly and are not clipped 39. Balanced composition: No large empty voids or overcrowded regions 40. Arrow routing clean: Arrows cross other arrows rather than boxes or main labels 41. No empty placeholders: No empty cards, blank containers, or decorative sections 42. No duplicated text: Same explanatory text not repeated across multiple blocks 43. Text is lean: High-value words only; no filler phrases
Delivery
44. Source kept: .excalidraw file is preserved and a hosted edit URL is provided 45. Validation passed: validate_excalidraw.py exited 0 before URL was generated 46. Animation (if asked): Hosted animate URL provided when user requests animation 47. PNG optional: PNG export created only if the user requested an image
Route Guide Template
Use this structure for every route guide.
Required order
1. Use this when 2. Output should look like 3. Shape vocabulary 4. ## One Big Idea 5. ## Best Default Layout 6. ## Must Show 7. ## Never Draw 8. ## Canonical Recipes 9. ## Hard Limits 10. ## Bad vs Better Prompt 11. ## Cut Ruthlessly 12. ## Minimum Viable Output
Allowed extras
## Special Rule## Fundamental Shape Mapping- domain-specific safety notes
Use extras only when the route genuinely needs them.
Sales Funnel Diagrams
Use this when the goal is to explain movement from awareness to conversion with visible drop-off or handoff points.
Output should look like a narrowing path with one clear conversion target and obvious leak points.
Shape vocabulary: see references/fundamental-shapes.md.
One Big Idea
One funnel diagram should explain one conversion journey only.
Best Default Layout
Default flow: top-wide to bottom-narrow
[Sources Sources Sources]
|
[Qualified]
|
[Converted]Reveal order: 1. Sources 2. Qualification stages 3. Leak or friction points 4. Conversion target
Must Show
- 3-6 funnel stages
- 1 visible conversion target
- 1 major leak or friction callout
- clear marketing-to-sales handoff if relevant
Never Draw
- process maps with no narrowing logic
- equal weight for top and bottom stages
- dense percentages on every step
Canonical Recipes
1. Simple funnel
Structure: Sources -> 3-5 stages -> conversion
2. Multi-channel funnel
Structure: 3 sources -> shared funnel -> one handoff -> conversion
3. Funnel comparison
Structure: Current funnel | improved funnel -> one leak fixed
Use comparison when possible. Current vs improved is usually clearer than a single overloaded funnel.
Hard Limits
- Funnel stages: 3-6
- source channels shown: 3 max
- metric callouts: 3 max
- leak callouts: 1-2
Bad vs Better Prompt
Bad: "Create a sales funnel diagram."
Better: "Create a hand-drawn sales funnel from ad click to booked demo with 4 stages, 1 major drop-off callout, and a clear sales handoff."
Cut Ruthlessly
Remove in this order: 1. extra percentages 2. weak secondary channels 3. explanatory text that repeats the stage labels
Minimum Viable Output
- 1 source region
- 3 stages
- 1 leak callout
- 1 conversion target
Acceptance Check
- Does the funnel visibly narrow?
- Is the conversion goal unmistakable?
- Is the biggest drop-off or friction point visible?
- Can someone explain the path in under 30 seconds?
UX Designer Diagrams
Use this when the goal is to explain screens, user flow, navigation, or interaction logic.
Output should look like one dominant path, a small number of screens or states, and one visible success outcome.
Shape vocabulary: see references/fundamental-shapes.md.
One Big Idea
One diagram should explain one journey, not the whole product.
Best Default Layout
Default flow: left-to-right
[Entry] -> [Screen] -> [Decision] -> [Success]
\
[Failure]Reveal order: 1. Entry point 2. Main path 3. Branch or failure state 4. Success outcome
Must Show
- 1 main path
- 3-5 primary screens or states
- 1 failure or alternate branch if it matters
- 1 clear end state
Never Draw
- page layouts detailed like real UI comps
- too many annotations inside screens
- flows where the error state is missing
Canonical Recipes
1. Wireflow
Structure: Entry -> 3-5 screens -> 1 branch -> success
2. Sitemap sketch
Structure: Homepage -> primary sections -> 1 deeper level only
3. Before/after UX comparison
Structure: Current flow | Improved flow -> one visible simplification
Use comparison when possible. Current vs improved often explains UX more simply than one giant flowchart.
Hard Limits
- Screens/states: 3-5
- Branches from main path: 1-2
- annotations per screen: 2 max
- primary arrows: keep one dominant direction
Bad vs Better Prompt
Bad: "Create a UX flow for signup."
Better: "Create a hand-drawn wireflow for signup with 4 screens, 1 failure branch, and 1 success state, using a left-to-right layout."
Cut Ruthlessly
Remove in this order: 1. detailed UI chrome 2. minor notes inside screens 3. low-value branches
Minimum Viable Output
- 1 entry point
- 3 screens
- 1 decision or branch
- 1 success state
Acceptance Check
- Is the main user path obvious before reading all labels?
- Are screens distinct without excessive detail?
- Is the failure branch visible if it matters?
- Does the diagram end in a clear outcome?
.venv/
__pycache__/
*.pyc
[project]
name = "excalidraw-render"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"playwright>=1.40.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
markers = [
"slow: tests that launch Playwright/Chromium (deselect with -m 'not slow')",
]
Related skills
FAQ
What files does hand-drawn-diagrams produce?
hand-drawn-diagrams produces diagram.excalidraw and diagram.animationinfo.json as required workflow artifacts, plus a hosted Excalidraw edit URL. Developers can also request PNG or SVG exports of the generated architecture, workflow, or UX blueprint diagram.
Which diagram types does hand-drawn-diagrams support?
hand-drawn-diagrams supports monochrome hand-drawn architecture, workflow, and UX blueprint diagrams generated from natural-language prompts. The skill routes to an appropriate diagram type, validates layout, and delivers sketch-style Excalidraw output rather than polished vector