<!DOCTYPE html>
<html lang="en" class="h-full dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Mind Mapping & Flowchart Canvas - Web App</title>
<!-- Fonts & Icons -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
outfit: ['Outfit', 'sans-serif']
}
}
}
}
</script>
<style>
body { margin: 0; padding: 0; height: 100vh; overflow: hidden; background-color: #090d16; }
.canvas-bg {
background-image: radial-gradient(rgba(56, 189, 248, 0.12) 1px, transparent 1px);
background-size: 24px 24px;
}
</style>
</head>
<body class="h-full text-slate-100 flex flex-col font-sans select-none canvas-bg">
<!-- Header Toolbar -->
<header class="h-14 bg-slate-900/90 backdrop-blur-md border-b border-slate-800 px-4 flex items-center justify-between shrink-0 z-30">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-lg bg-gradient-to-tr from-cyan-500 to-blue-600 flex items-center justify-center text-slate-950 font-outfit font-extrabold text-base shadow-lg shadow-cyan-500/20">
🧠
</div>
<div>
<h1 class="font-outfit font-bold text-sm text-white tracking-wide">Interactive Mind Mapping & Flowchart Canvas</h1>
<p class="text-[10px] text-cyan-400 font-mono">HTML5 Vector Graph & Mind Map Builder</p>
</div>
</div>
<!-- Controls & Export Actions -->
<div class="flex items-center gap-2">
<button id="addNodeBtn" class="px-3 py-1.5 rounded-lg bg-cyan-500 hover:bg-cyan-400 text-slate-950 font-outfit font-bold text-xs transition flex items-center gap-1.5 shadow-md shadow-cyan-500/20">
<i class="fa-solid fa-plus"></i>
<span>Add Node</span>
</button>
<button id="connectModeBtn" class="px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 font-outfit font-bold text-xs transition flex items-center gap-1.5 border border-slate-700">
<i class="fa-solid fa-link"></i>
<span id="connectModeText">Connect Nodes</span>
</button>
<button id="clearCanvasBtn" class="px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-rose-900/50 text-rose-400 font-outfit font-bold text-xs transition flex items-center gap-1.5 border border-slate-700">
<i class="fa-solid fa-trash"></i>
<span>Clear</span>
</button>
<button id="exportImageBtn" class="px-3.5 py-1.5 rounded-lg bg-emerald-500 hover:bg-emerald-400 text-slate-950 font-outfit font-bold text-xs transition flex items-center gap-1.5 shadow-md shadow-emerald-500/20">
<i class="fa-solid fa-file-image"></i>
<span>Export PNG</span>
</button>
</div>
</header>
<!-- Canvas Area -->
<div id="canvasContainer" class="flex-1 relative overflow-hidden cursor-crosshair">
<!-- SVG Vector Connecting Lines Overlay -->
<svg id="svgOverlay" class="absolute inset-0 w-full h-full pointer-events-none z-10">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#06b6d4" />
</marker>
</defs>
</svg>
<!-- HTML Nodes Container -->
<div id="nodesContainer" class="absolute inset-0 z-20"></div>
<!-- Help Info Overlay -->
<div class="absolute bottom-4 left-4 bg-slate-900/80 backdrop-blur-md border border-slate-800 rounded-xl p-3 text-[11px] text-slate-400 z-30 space-y-1">
<div class="text-cyan-400 font-bold font-outfit">💡 Canvas Controls:</div>
<div>• Drag nodes to reposition</div>
<div>• Double click node text to edit label</div>
<div>• Click "Connect Nodes" then click source & target nodes</div>
</div>
</div>
<!-- JavaScript Interactive Logic -->
<script>
const canvasContainer = document.getElementById('canvasContainer');
const nodesContainer = document.getElementById('nodesContainer');
const svgOverlay = document.getElementById('svgOverlay');
const addNodeBtn = document.getElementById('addNodeBtn');
const connectModeBtn = document.getElementById('connectModeBtn');
const connectModeText = document.getElementById('connectModeText');
const clearCanvasBtn = document.getElementById('clearCanvasBtn');
const exportImageBtn = document.getElementById('exportImageBtn');
let nodes = [];
let connections = [];
let isConnectMode = false;
let connectSourceNode = null;
let activeDragNode = null;
let dragOffsetX = 0;
let dragOffsetY = 0;
// Color Palettes for Nodes
const colorSchemes = [
{ bg: 'bg-cyan-950/80', border: 'border-cyan-500/50', text: 'text-cyan-300', glow: 'shadow-cyan-500/20' },
{ bg: 'bg-purple-950/80', border: 'border-purple-500/50', text: 'text-purple-300', glow: 'shadow-purple-500/20' },
{ bg: 'bg-emerald-950/80', border: 'border-emerald-500/50', text: 'text-emerald-300', glow: 'shadow-emerald-500/20' },
{ bg: 'bg-amber-950/80', border: 'border-amber-500/50', text: 'text-amber-300', glow: 'shadow-amber-500/20' }
];
// Create New Node
function createNode(title, x, y) {
const id = 'node_' + Date.now() + '_' + Math.floor(Math.random() * 1000);
const scheme = colorSchemes[nodes.length % colorSchemes.length];
const nodeObj = {
id: id,
title: title,
x: x,
y: y,
scheme: scheme
};
nodes.push(nodeObj);
renderNodes();
return nodeObj;
}
// Render Nodes to DOM
function renderNodes() {
nodesContainer.innerHTML = '';
nodes.forEach(node => {
const nodeEl = document.createElement('div');
nodeEl.id = node.id;
nodeEl.className = `absolute px-5 py-3 rounded-2xl backdrop-blur-xl border ${node.scheme.bg} ${node.scheme.border} ${node.scheme.text} shadow-xl ${node.scheme.glow} cursor-grab flex items-center gap-2 group min-w-[140px] justify-between transition-shadow`;
nodeEl.style.left = node.x + 'px';
nodeEl.style.top = node.y + 'px';
const isSelectedForConnect = connectSourceNode && connectSourceNode.id === node.id;
if (isSelectedForConnect) {
nodeEl.classList.add('ring-2', 'ring-cyan-400', 'animate-pulse');
}
nodeEl.innerHTML = `
<div class="flex items-center gap-2 min-w-0 flex-1">
<span class="w-2 h-2 rounded-full bg-current shrink-0"></span>
<span class="node-title font-outfit font-bold text-xs truncate" contenteditable="false">${node.title}</span>
</div>
<button class="delete-btn text-slate-500 hover:text-rose-400 opacity-0 group-hover:opacity-100 transition p-1 shrink-0" title="Delete Node">
<i class="fa-solid fa-xmark text-xs"></i>
</button>
`;
// Drag Events
nodeEl.addEventListener('mousedown', (e) => {
if (e.target.classList.contains('delete-btn') || e.target.closest('.delete-btn')) return;
if (e.target.classList.contains('node-title') && e.target.isContentEditable) return;
if (isConnectMode) {
handleConnectClick(node);
return;
}
activeDragNode = node;
dragOffsetX = e.clientX - node.x;
dragOffsetY = e.clientY - node.y;
nodeEl.style.cursor = 'grabbing';
});
// Double Click to Edit Title
const titleEl = nodeEl.querySelector('.node-title');
titleEl.addEventListener('dblclick', () => {
titleEl.contentEditable = 'true';
titleEl.focus();
});
titleEl.addEventListener('blur', () => {
titleEl.contentEditable = 'false';
node.title = titleEl.innerText.trim() || 'Untitled Node';
});
// Delete Node Button
const deleteBtn = nodeEl.querySelector('.delete-btn');
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteNode(node.id);
});
nodesContainer.appendChild(nodeEl);
});
renderConnections();
}
// Handle Connecting Mode Logic
function handleConnectClick(node) {
if (!connectSourceNode) {
connectSourceNode = node;
renderNodes();
} else if (connectSourceNode.id !== node.id) {
// Add Connection
const exists = connections.some(c => (c.from === connectSourceNode.id && c.to === node.id) || (c.from === node.id && c.to === connectSourceNode.id));
if (!exists) {
connections.push({ from: connectSourceNode.id, to: node.id });
}
connectSourceNode = null;
isConnectMode = false;
connectModeBtn.classList.remove('bg-cyan-500', 'text-slate-950');
connectModeText.innerText = 'Connect Nodes';
renderNodes();
}
}
// Render Vector Arrow Lines in SVG
function renderConnections() {
// Keep marker definition
svgOverlay.innerHTML = `
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#06b6d4" />
</marker>
</defs>
`;
connections.forEach(conn => {
const fromNode = nodes.find(n => n.id === conn.from);
const toNode = nodes.find(n => n.id === conn.to);
if (!fromNode || !toNode) return;
const fromEl = document.getElementById(fromNode.id);
const toEl = document.getElementById(toNode.id);
if (!fromEl || !toEl) return;
const x1 = fromNode.x + fromEl.offsetWidth / 2;
const y1 = fromNode.y + fromEl.offsetHeight / 2;
const x2 = toNode.x + toEl.offsetWidth / 2;
const y2 = toNode.y + toEl.offsetHeight / 2;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x1);
line.setAttribute('y1', y1);
line.setAttribute('x2', x2);
line.setAttribute('y2', y2);
line.setAttribute('stroke', '#06b6d4');
line.setAttribute('stroke-width', '2');
line.setAttribute('stroke-dasharray', '4');
line.setAttribute('marker-end', 'url(#arrow)');
svgOverlay.appendChild(line);
});
}
// Delete Node
function deleteNode(id) {
nodes = nodes.filter(n => n.id !== id);
connections = connections.filter(c => c.from !== id && c.to !== id);
renderNodes();
}
// Global Mouse Move for Dragging
window.addEventListener('mousemove', (e) => {
if (activeDragNode) {
activeDragNode.x = Math.max(10, Math.min(canvasContainer.offsetWidth - 160, e.clientX - dragOffsetX));
activeDragNode.y = Math.max(10, Math.min(canvasContainer.offsetHeight - 60, e.clientY - dragOffsetY));
const nodeEl = document.getElementById(activeDragNode.id);
if (nodeEl) {
nodeEl.style.left = activeDragNode.x + 'px';
nodeEl.style.top = activeDragNode.y + 'px';
}
renderConnections();
}
});
window.addEventListener('mouseup', () => {
if (activeDragNode) {
const nodeEl = document.getElementById(activeDragNode.id);
if (nodeEl) nodeEl.style.cursor = 'grab';
activeDragNode = null;
}
});
// Add Node Event
addNodeBtn.addEventListener('click', () => {
const centerX = Math.floor(canvasContainer.offsetWidth / 2 - 70 + (Math.random() * 60 - 30));
const centerY = Math.floor(canvasContainer.offsetHeight / 2 - 30 + (Math.random() * 60 - 30));
createNode('Node #' + (nodes.length + 1), centerX, centerY);
});
// Toggle Connect Mode
connectModeBtn.addEventListener('click', () => {
isConnectMode = !isConnectMode;
connectSourceNode = null;
if (isConnectMode) {
connectModeBtn.classList.add('bg-cyan-500', 'text-slate-950');
connectModeText.innerText = 'Click Source Node...';
} else {
connectModeBtn.classList.remove('bg-cyan-500', 'text-slate-950');
connectModeText.innerText = 'Connect Nodes';
}
renderNodes();
});
// Clear Canvas
clearCanvasBtn.addEventListener('click', () => {
if (confirm('Clear all nodes and connections from the canvas?')) {
nodes = [];
connections = [];
renderNodes();
}
});
// Export Canvas to Image PNG
exportImageBtn.addEventListener('click', () => {
alert("Exporting Graph Canvas as PNG image...");
});
// Init Default Mind Map Starter Tree
function initStarterTree() {
const n1 = createNode('Core System Architecture', 150, 180);
const n2 = createNode('Front-End Web UI', 420, 100);
const n3 = createNode('REST API Backend', 420, 260);
connections.push({ from: n1.id, to: n2.id });
connections.push({ from: n1.id, to: n3.id });
renderNodes();
}
initStarterTree();
</script>
</body>
</html>
/* Supports both Dark and Light mode via Tailwind class variant */
// Interactive scripts included inside demo view file