You have a big topic and a blank page. Ideas come in any order.
So you draw a circle in the middle and start adding lines. Twenty minutes later the page is full, the writing is tiny, and the best idea is squeezed into a corner.
A mind map should help you see the shape of a topic. On paper it often ends up hiding it.
These days people ask ChatGPT, Claude or Gemini for it and get a finished page in one reply. You write a plain outline; the page draws the map, and each branch opens when you tap it.
Here is a mind map template for a school event. Tap a branch to open or close it.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mind map: science fair</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; background: radial-gradient(circle at 15% 0%, #e4f3ff, transparent 45%), radial-gradient(circle at 100% 40%, #fff0e3, transparent 40%), #fbfaf7; color: #1f2433; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
.page { max-width: 760px; margin: 0 auto; padding: 16px 12px 26px; }
.top { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 4px; }
h1 { margin: 0; font-size: 19px; }
.tools { display: flex; gap: 6px; }
.tools button { border: 0; border-radius: 10px; padding: 8px 10px; background: #1f2433; color: #fff; font: 600 13px system-ui; cursor: pointer; }
.tools button.alt { background: #e9e6df; color: #1f2433; }
.hint { font-size: 13px; color: #6a7080; margin: 0 0 10px; }
.stage { position: relative; transition: height .38s ease; }
.stage svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.node { position: absolute; left: 0; top: 0; padding: 8px 10px; border-radius: 12px; font: 600 13px/1.25 system-ui; text-align: center; cursor: default; user-select: none; will-change: transform, opacity; }
.node.kids { cursor: pointer; }
.node.d0 { background: #1f2433; color: #fff; font-size: 16px; font-weight: 800; padding: 14px 12px; border-radius: 18px; box-shadow: 0 10px 24px rgba(31,36,51,.25); cursor: pointer; }
.node.d1 { color: #fff; font-weight: 700; font-size: 14px; border-radius: 99px; padding: 9px 12px; box-shadow: 0 6px 14px rgba(0,0,0,.12); }
.node.d2, .node.d3 { background: #fff; border: 2px solid; font-weight: 500; box-shadow: 0 3px 8px rgba(0,0,0,.06); }
.node.kids:hover { filter: brightness(1.05); }
.badge { display: inline-block; margin-left: 6px; min-width: 20px; padding: 1px 6px; border-radius: 99px; background: rgba(255,255,255,.3); font-size: 11px; font-weight: 800; }
.d2 .badge, .d3 .badge { background: #eef0f4; color: #1f2433; }
.outline { margin-top: 18px; padding: 14px 16px; border-radius: 16px; background: rgba(255,255,255,.8); border: 1px solid #ece8df; font-size: 14px; }
.outline h2 { margin: 0 0 6px; font-size: 12px; letter-spacing: .1em; text-transform: uppercase; color: #6a7080; }
.outline ul { margin: 2px 0; padding-left: 20px; }
.outline > ul { padding-left: 4px; list-style: none; }
.outline li { margin: 2px 0; }
</style>
</head>
<body>
<main class="page">
<div class="top">
<h1>Science fair plan</h1>
<div class="tools"><button id="openAll">Open all</button><button class="alt" id="closeAll">Close all</button></div>
</div>
<p class="hint">Tap a branch to open or close it. The map is drawn from the outline below.</p>
<div class="stage" id="stage"><svg id="lines"></svg></div>
<section class="outline">
<h2>The outline</h2>
<ul id="outline">
<li>Science fair 2027
<ul>
<li>Date and place
<ul><li>Friday 12 March</li><li>School hall, 1-4 pm</li><li>Set-up from 11 am</li></ul>
</li>
<li>Projects
<ul><li>Grade 4: plants and light</li><li>Grade 5: simple machines</li><li>Grade 6: free choice</li><li>Max 3 students per table</li></ul>
</li>
<li>Volunteers
<ul><li>Judges: 6 parents</li><li>Set-up crew: Mr Okafor's class</li><li>Snack table: PTA</li></ul>
</li>
<li>Budget
<ul><li>Poster boards: $90</li><li>Ribbons and prizes: $60</li><li>Snacks: $40</li></ul>
</li>
<li>Getting the word out
<ul><li>Newsletter, 2 weeks before</li><li>Posters in the corridors</li><li>Invite the other school</li></ul>
</li>
</ul>
</li>
</ul>
</section>
</main>
<script>
const stage = document.getElementById('stage'), svg = document.getElementById('lines');
const PAL = ['#ff6b4a', '#18a999', '#7357f6', '#e9a400', '#2f80ed', '#e2468b'];
const open = { 'r.0': true };
const nodes = {};
let tree, anim;
// Read the tree from the visible outline. Editing a line there changes the map.
function parse(list, path) {
return [...list.children].filter((li) => li.tagName === 'LI').map((li, i) => {
const key = path + '.' + i; let label = '';
li.childNodes.forEach((n) => { if (n.nodeType === 3 || (n.nodeType === 1 && !/^(UL|OL)$/.test(n.tagName))) label += n.textContent; });
const sub = li.querySelector(':scope > ul, :scope > ol');
return { key, label: label.replace(/\s+/g, ' ').trim(), kids: sub ? parse(sub, key) : [] };
}).filter((n) => n.label);
}
const vis = (n) => (n.key === 'r' || open[n.key]) ? n.kids : [];
function ensure(n, d, color) {
let e = nodes[n.key];
if (!e) {
const el = document.createElement('div'); stage.append(el);
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('fill', 'none'); svg.append(path);
e = nodes[n.key] = { el, path, isNew: true };
el.addEventListener('click', () => toggle(e.n));
}
e.n = n; e.leaving = false;
const el = e.el;
el.className = 'node d' + Math.min(d, 3) + (n.kids.length ? ' kids' : '');
el.textContent = n.label;
if (n.kids.length && d > 0) { const b = document.createElement('span'); b.className = 'badge'; b.textContent = open[n.key] ? '−' : '+' + n.kids.length; el.append(b); }
if (d === 1) el.style.background = color;
if (d >= 2) { el.style.borderColor = color; el.style.background = color + '14'; }
e.path.setAttribute('stroke', color || '#999'); e.path.setAttribute('stroke-width', d === 1 ? 4 : 2.2); e.path.setAttribute('stroke-linecap', 'round');
e.path.style.opacity = d === 1 ? .55 : .45;
return e;
}
function layout() {
const W = stage.clientWidth, two = W >= 620;
const wOf = (d) => two ? [120, 118, 120, 120][Math.min(d, 3)] : [96, 112, 116, 116][Math.min(d, 3)];
const G = two ? 18 : 14, VG = 8, BG = 14;
const out = {};
function measure(n, d, color) { const e = ensure(n, d, color); e.el.style.width = wOf(d) + 'px'; return e.el.offsetHeight; }
function tall(n, d, color) {
n._h = measure(n, d, color); const ks = vis(n);
if (!ks.length) return (n._t = n._h);
const s = ks.reduce((a, k) => a + tall(k, d + 1, color), 0) + VG * (ks.length - 1);
return (n._t = Math.max(n._h, s));
}
function place(n, d, x, top, side, color, parent) {
const w = wOf(d), bx = side > 0 ? x : x - w, cy = top + n._t / 2;
out[n.key] = { x: bx, y: cy - n._h / 2, w, h: n._h, parent, side, d };
const ks = vis(n); let t = top + (n._t - (ks.reduce((a, k) => a + k._t, 0) + VG * (ks.length - 1))) / 2;
ks.forEach((k) => { place(k, d + 1, side > 0 ? bx + w + G : bx - G, t, side, color, n.key); t += k._t + VG; });
}
const rootH = measure(tree, 0);
const br = tree.kids;
br.forEach((b, i) => tall(b, 1, PAL[i % PAL.length]));
const total = br.reduce((a, b) => a + b._t, 0);
let right = [], left = [], acc = 0;
br.forEach((b) => { if (!two || acc < total / 2) { right.push(b); acc += b._t; } else left.push(b); });
const sideH = (arr) => arr.reduce((a, b) => a + b._t, 0) + BG * Math.max(0, arr.length - 1);
const H = Math.max(sideH(right), sideH(left), rootH) + 24;
const rw = wOf(0), rx = two ? (W - rw) / 2 : 0;
out[tree.key] = { x: rx, y: H / 2 - rootH / 2, w: rw, h: rootH, parent: null, d: 0 };
const run = (arr, side) => { let t = H / 2 - sideH(arr) / 2; arr.forEach((b) => { place(b, 1, side > 0 ? rx + rw + G : rx - G, t, side, PAL[br.indexOf(b) % PAL.length], tree.key); t += b._t + BG; }); };
run(right, 1); run(left, -1);
stage.style.height = H + 'px';
return out;
}
function anc(key, tgt) { let k = key; while (k.includes('.')) { k = k.slice(0, k.lastIndexOf('.')); if (tgt[k]) return tgt[k]; } return null; }
function render(instant) {
const tgt = layout();
for (const [k, e] of Object.entries(nodes)) {
const t = tgt[k];
if (t) {
if (e.isNew) { const p = t.parent && (nodes[t.parent].now || tgt[t.parent]); e.now = p && !instant ? { x: p.x + p.w / 2 - t.w / 2, y: p.y + p.h / 2 - t.h / 2, o: 0, w: t.w, h: t.h } : { x: t.x, y: t.y, o: 0, w: t.w, h: t.h }; e.isNew = false; }
e.from = { ...e.now }; e.to = { x: t.x, y: t.y, o: 1, w: t.w, h: t.h }; e.t = t;
} else {
const p = anc(k, tgt);
e.leaving = true; e.from = { ...e.now };
e.to = p ? { x: p.x + p.w / 2 - e.now.w / 2, y: p.y + p.h / 2 - e.now.h / 2, o: 0, w: e.now.w, h: e.now.h } : { ...e.now, o: 0 };
}
}
cancelAnimationFrame(anim);
const t0 = performance.now(), D = instant ? 1 : 420;
const ease = (x) => 1 - Math.pow(1 - x, 3);
(function frame(now) {
const p = Math.min(1, (now - t0) / D), q = ease(p);
for (const e of Object.values(nodes)) {
const f = e.from, to = e.to;
e.now = { x: f.x + (to.x - f.x) * q, y: f.y + (to.y - f.y) * q, o: f.o + (to.o - f.o) * q, w: to.w, h: to.h };
e.el.style.transform = `translate(${e.now.x}px, ${e.now.y}px)`; e.el.style.opacity = e.now.o;
}
for (const [k, e] of Object.entries(nodes)) {
const pk = e.t && e.t.parent; const pe = pk && nodes[pk];
if (!pe) { e.path.setAttribute('d', ''); continue; }
const a = pe.now, b = e.now, side = e.t.side;
const x1 = side > 0 ? a.x + a.w : a.x, y1 = a.y + a.h / 2, x2 = side > 0 ? b.x : b.x + b.w, y2 = b.y + b.h / 2, mx = (x1 + x2) / 2;
e.path.setAttribute('d', `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`); e.path.style.opacity = b.o * (e.t.d === 1 ? .55 : .45);
}
if (p < 1) anim = requestAnimationFrame(frame);
else for (const [k, e] of Object.entries(nodes)) if (e.leaving) { e.el.remove(); e.path.remove(); delete nodes[k]; }
})(t0);
}
function toggle(n) {
if (!n.kids.length) return;
if (n.key === 'r') { const any = n.kids.some((k) => open[k.key]); n.kids.forEach((k) => { open[k.key] = !any; }); }
else open[n.key] = !open[n.key];
render();
}
function setAll(v) { (function walk(n) { n.kids.forEach((k) => { open[k.key] = v; walk(k); }); })(tree); render(); }
document.getElementById('openAll').onclick = () => setAll(true);
document.getElementById('closeAll').onclick = () => setAll(false);
function build(instant) {
const outline = document.getElementById('outline');
tree = parse(outline, 'r')[0]; tree.key = 'r';
const fix = (n, p) => { n.kids.forEach((k, i) => { k.key = p + '.' + i; fix(k, k.key); }); }; fix(tree, 'r');
render(instant);
}
let timer;
new MutationObserver(() => { clearTimeout(timer); timer = setTimeout(() => build(), 150); })
.observe(document.getElementById('outline'), { subtree: true, characterData: true, childList: true });
let lastW = 0;
addEventListener('resize', () => { if (stage.clientWidth !== lastW) { lastW = stage.clientWidth; build(true); } });
build(true); lastW = stage.clientWidth;
</script>
</body>
</html>
Three things that keep a mind map readable
1. Write it as an outline first. Topic, branches, points, each one indent deeper. It takes two minutes in any notes app, and it is the part you will edit later. The map is just a picture of it.
2. Five or so branches, short labels. "Budget" reads at a glance. "Things we still need to pay for before March" does not. Keep the branch names to a word or two and put the detail in the points underneath.
3. Start closed. A map with every point open is the paper problem again. Showing the main branches first gives the shape; people open the one they care about.

Make it with AI
Open ChatGPT, Claude or Gemini and paste this. Change the words in capitals and paste your outline at the end.
Make one complete HTML file: an interactive mind map.
Central topic: YOUR TOPIC
The map shows the topic in the middle and main
branches around it, each in its own colour, joined
by curved lines. Tap a branch to open or close its
points, with a short animation. Add "Open all" and
"Close all" buttons. Start with branches closed.
Show my outline as a plain nested list at the
bottom of the page, and draw the map from that
list.
Rules: all CSS and JavaScript inside the file, no
images or fonts from other sites, no libraries,
nothing sent anywhere, works on a phone.
Use only my outline. Do not add branches or points.
Outline: PASTE YOUR INDENTED LIST HERE
"Draw the map from that list" matters most. It keeps every word as plain text on the page, so changing the map later means editing one line of a list instead of digging through code.
A mind map template can do more than show. This one is an essay plan, with one extra line in the prompt: tap a point to tick it off as you write.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mind map: essay plan</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; background: radial-gradient(circle at 50% 0%, #2b2f5c, transparent 60%), #14152b; color: #eef0ff; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
.page { max-width: 760px; margin: 0 auto; padding: 16px 12px 26px; }
.top { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 6px; }
h1 { margin: 0; font-size: 19px; }
.prog { flex: none; font-size: 13px; color: #c9ccff; background: rgba(255,255,255,.08); border-radius: 99px; padding: 6px 10px; }
.prog b { color: #b6f36a; }
.meter { height: 6px; border-radius: 9px; background: rgba(255,255,255,.1); overflow: hidden; margin-bottom: 8px; }
.meter i { display: block; height: 100%; width: 0; background: linear-gradient(90deg, #6a7bff, #b6f36a); transition: width .4s; }
.hint { font-size: 13px; color: #a9acd6; margin: 0 0 10px; }
.tools { display: flex; gap: 6px; margin-bottom: 8px; }
.tools button { border: 0; border-radius: 10px; padding: 8px 10px; background: #eef0ff; color: #14152b; font: 600 13px system-ui; cursor: pointer; }
.tools button.alt { background: rgba(255,255,255,.1); color: #eef0ff; }
.stage { position: relative; transition: height .38s ease; }
.stage svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.node { position: absolute; left: 0; top: 0; padding: 8px 10px; border-radius: 12px; font: 600 13px/1.25 system-ui; text-align: center; cursor: pointer; user-select: none; will-change: transform, opacity; transition: filter .3s; }
.node.d0 { background: linear-gradient(135deg, #fefefe, #dfe3ff); color: #14152b; font-size: 15px; font-weight: 800; padding: 14px 12px; border-radius: 18px; box-shadow: 0 0 0 6px rgba(255,255,255,.08), 0 12px 30px rgba(0,0,0,.4); }
.node.d1 { color: #fff; font-weight: 700; font-size: 14px; border-radius: 99px; padding: 9px 12px; box-shadow: 0 0 18px rgba(0,0,0,.35); }
.node.d2, .node.d3 { background: #1e2042; border: 2px solid; color: #e6e8ff; font-weight: 500; text-align: left; padding-left: 30px; }
.node.d2::before, .node.d3::before { content: ""; position: absolute; left: 9px; top: 50%; width: 14px; height: 14px; margin-top: -7px; border-radius: 50%; border: 2px solid currentColor; opacity: .55; }
.node.done { color: #b6f36a; }
.node.done::before { background: #b6f36a; border-color: #b6f36a; opacity: 1; box-shadow: inset 0 0 0 3px #1e2042; }
.node.dim { filter: grayscale(.8) brightness(.55); }
.badge { display: inline-block; margin-left: 6px; min-width: 20px; padding: 1px 6px; border-radius: 99px; background: rgba(255,255,255,.25); font-size: 11px; font-weight: 800; }
.outline { margin-top: 18px; padding: 14px 16px; border-radius: 16px; background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.1); font-size: 14px; color: #d9dbf5; }
.outline h2 { margin: 0 0 6px; font-size: 12px; letter-spacing: .1em; text-transform: uppercase; color: #a9acd6; }
.outline ul { margin: 2px 0; padding-left: 20px; }
.outline > ul { padding-left: 4px; list-style: none; }
.outline li { margin: 2px 0; }
</style>
</head>
<body>
<main class="page">
<div class="top">
<h1>Essay plan: should school start later?</h1>
<span class="prog"><b id="doneN">0</b> of <span id="allN">0</span> points ready</span>
</div>
<div class="meter"><i id="meter"></i></div>
<p class="hint">Tap a branch to open it and focus on it. Tap a point to tick it off. The map is drawn from the outline below.</p>
<div class="tools"><button id="openAll">Open all</button><button class="alt" id="closeAll">Close all</button></div>
<div class="stage" id="stage"><svg id="lines"></svg></div>
<section class="outline">
<h2>The outline</h2>
<ul id="outline">
<li>Should school start later?
<ul>
<li>Opening
<ul><li>The 7:15 bus story</li><li>My question: would 9:30 work?</li><li>My answer: yes, with limits</li></ul>
</li>
<li>Tired mornings
<ul><li>Ask 20 classmates when they wake up</li><li>First-period yawns: my notes</li></ul>
</li>
<li>Better first lessons
<ul><li>Ask two teachers what they see</li><li>My own maths grades by period</li></ul>
</li>
<li>The other side
<ul><li>Parents who start work early</li><li>Shared buses</li><li>Sports end in the dark</li></ul>
</li>
<li>Ending
<ul><li>Back to the bus story</li><li>Try it first: late Mondays</li></ul>
</li>
</ul>
</li>
</ul>
</section>
</main>
<script>
const stage = document.getElementById('stage'), svg = document.getElementById('lines');
const PAL = ['#ff6fae', '#4fc3ff', '#9b7bff', '#ffb547', '#3ddc97', '#ff7a59'];
const done = {};
let focus = null;
const open = {};
const nodes = {};
let tree, anim, e;
// Read the tree from the visible outline. Editing a line there changes the map.
function parse(list, path) {
return [...list.children].filter((li) => li.tagName === 'LI').map((li, i) => {
const key = path + '.' + i; let label = '';
li.childNodes.forEach((n) => { if (n.nodeType === 3 || (n.nodeType === 1 && !/^(UL|OL)$/.test(n.tagName))) label += n.textContent; });
const sub = li.querySelector(':scope > ul, :scope > ol');
return { key, label: label.replace(/\s+/g, ' ').trim(), kids: sub ? parse(sub, key) : [] };
}).filter((n) => n.label);
}
const vis = (n) => (n.key === 'r' || open[n.key]) ? n.kids : [];
function ensure(n, d, color) {
let e = nodes[n.key];
if (!e) {
const el = document.createElement('div'); stage.append(el);
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('fill', 'none'); svg.append(path);
e = nodes[n.key] = { el, path, isNew: true };
el.addEventListener('click', () => toggle(e.n));
}
e.n = n; e.leaving = false;
const el = e.el;
el.className = 'node d' + Math.min(d, 3) + (n.kids.length ? ' kids' : '');
el.textContent = n.label;
if (n.kids.length && d > 0) { const b = document.createElement('span'); b.className = 'badge'; b.textContent = open[n.key] ? '−' : '+' + n.kids.length; el.append(b); }
if (d === 1) el.style.background = color;
if (d >= 2) { el.style.borderColor = color; }
if (!n.kids.length && done[n.key]) el.classList.add('done');
const br = n.key.split('.').slice(0, 2).join('.');
if (focus && d > 0 && br !== focus) el.classList.add('dim');
e.path.setAttribute('stroke', color || '#999'); e.path.setAttribute('stroke-width', d === 1 ? 4 : 2.2); e.path.setAttribute('stroke-linecap', 'round');
e.path.style.opacity = d === 1 ? .55 : .45;
return e;
}
function layout() {
const W = stage.clientWidth, two = W >= 620;
const wOf = (d) => two ? [120, 118, 120, 120][Math.min(d, 3)] : [96, 112, 116, 116][Math.min(d, 3)];
const G = two ? 18 : 14, VG = 8, BG = 14;
const out = {};
function measure(n, d, color) { const e = ensure(n, d, color); e.el.style.width = wOf(d) + 'px'; return e.el.offsetHeight; }
function tall(n, d, color) {
n._h = measure(n, d, color); const ks = vis(n);
if (!ks.length) return (n._t = n._h);
const s = ks.reduce((a, k) => a + tall(k, d + 1, color), 0) + VG * (ks.length - 1);
return (n._t = Math.max(n._h, s));
}
function place(n, d, x, top, side, color, parent) {
const w = wOf(d), bx = side > 0 ? x : x - w, cy = top + n._t / 2;
out[n.key] = { x: bx, y: cy - n._h / 2, w, h: n._h, parent, side, d };
const ks = vis(n); let t = top + (n._t - (ks.reduce((a, k) => a + k._t, 0) + VG * (ks.length - 1))) / 2;
ks.forEach((k) => { place(k, d + 1, side > 0 ? bx + w + G : bx - G, t, side, color, n.key); t += k._t + VG; });
}
const rootH = measure(tree, 0);
const br = tree.kids;
br.forEach((b, i) => tall(b, 1, PAL[i % PAL.length]));
const total = br.reduce((a, b) => a + b._t, 0);
let right = [], left = [], acc = 0;
br.forEach((b) => { if (!two || acc < total / 2) { right.push(b); acc += b._t; } else left.push(b); });
const sideH = (arr) => arr.reduce((a, b) => a + b._t, 0) + BG * Math.max(0, arr.length - 1);
const H = Math.max(sideH(right), sideH(left), rootH) + 24;
const rw = wOf(0), rx = two ? (W - rw) / 2 : 0;
out[tree.key] = { x: rx, y: H / 2 - rootH / 2, w: rw, h: rootH, parent: null, d: 0 };
const run = (arr, side) => { let t = H / 2 - sideH(arr) / 2; arr.forEach((b) => { place(b, 1, side > 0 ? rx + rw + G : rx - G, t, side, PAL[br.indexOf(b) % PAL.length], tree.key); t += b._t + BG; }); };
run(right, 1); run(left, -1);
stage.style.height = H + 'px';
return out;
}
function anc(key, tgt) { let k = key; while (k.includes('.')) { k = k.slice(0, k.lastIndexOf('.')); if (tgt[k]) return tgt[k]; } return null; }
function render(instant) {
const tgt = layout();
for (const [k, e] of Object.entries(nodes)) {
const t = tgt[k];
if (t) {
if (e.isNew) { const p = t.parent && (nodes[t.parent].now || tgt[t.parent]); e.now = p && !instant ? { x: p.x + p.w / 2 - t.w / 2, y: p.y + p.h / 2 - t.h / 2, o: 0, w: t.w, h: t.h } : { x: t.x, y: t.y, o: 0, w: t.w, h: t.h }; e.isNew = false; }
e.from = { ...e.now }; e.to = { x: t.x, y: t.y, o: 1, w: t.w, h: t.h }; e.t = t;
} else {
const p = anc(k, tgt);
e.leaving = true; e.from = { ...e.now };
e.to = p ? { x: p.x + p.w / 2 - e.now.w / 2, y: p.y + p.h / 2 - e.now.h / 2, o: 0, w: e.now.w, h: e.now.h } : { ...e.now, o: 0 };
}
}
cancelAnimationFrame(anim);
const t0 = performance.now(), D = instant ? 1 : 420;
const ease = (x) => 1 - Math.pow(1 - x, 3);
(function frame(now) {
const p = Math.min(1, (now - t0) / D), q = ease(p);
for (const e of Object.values(nodes)) {
const f = e.from, to = e.to;
e.now = { x: f.x + (to.x - f.x) * q, y: f.y + (to.y - f.y) * q, o: f.o + (to.o - f.o) * q, w: to.w, h: to.h };
e.el.style.transform = `translate(${e.now.x}px, ${e.now.y}px)`; e.el.style.opacity = e.now.o;
}
for (const [k, e] of Object.entries(nodes)) {
const pk = e.t && e.t.parent; const pe = pk && nodes[pk];
if (!pe) { e.path.setAttribute('d', ''); continue; }
const a = pe.now, b = e.now, side = e.t.side;
const x1 = side > 0 ? a.x + a.w : a.x, y1 = a.y + a.h / 2, x2 = side > 0 ? b.x : b.x + b.w, y2 = b.y + b.h / 2, mx = (x1 + x2) / 2;
e.path.setAttribute('d', `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`); e.path.style.opacity = b.o * (e.t.d === 1 ? .55 : .45);
}
if (p < 1) anim = requestAnimationFrame(frame);
else for (const [k, e] of Object.entries(nodes)) if (e.leaving) { e.el.remove(); e.path.remove(); delete nodes[k]; }
})(t0);
}
function toggle(n) {
if (!n.kids.length) { done[n.key] = !done[n.key]; e = nodes[n.key]; e.el.classList.toggle('done', !!done[n.key]); count(); return; }
if (n.key === 'r') { const any = n.kids.some((k) => open[k.key]); n.kids.forEach((k) => { open[k.key] = !any; }); focus = null; }
else { open[n.key] = !open[n.key]; focus = open[n.key] ? n.key : null; if (focus) tree.kids.forEach((k) => { if (k.key !== n.key) open[k.key] = false; }); }
render(); count();
}
function count() {
const leaves = []; (function walk(n) { if (!n.kids.length) leaves.push(n.key); n.kids.forEach(walk); })(tree);
const k = leaves.filter((x) => done[x]).length;
document.getElementById('doneN').textContent = k; document.getElementById('allN').textContent = leaves.length;
document.getElementById('meter').style.width = (leaves.length ? k / leaves.length * 100 : 0) + '%';
}
function setAll(v) { focus = null; (function walk(n) { n.kids.forEach((k) => { open[k.key] = v; walk(k); }); })(tree); render(); }
document.getElementById('openAll').onclick = () => setAll(true);
document.getElementById('closeAll').onclick = () => setAll(false);
function build(instant) {
const outline = document.getElementById('outline');
tree = parse(outline, 'r')[0]; tree.key = 'r';
const fix = (n, p) => { n.kids.forEach((k, i) => { k.key = p + '.' + i; fix(k, k.key); }); }; fix(tree, 'r');
render(instant); count();
}
let timer;
new MutationObserver(() => { clearTimeout(timer); timer = setTimeout(() => build(), 150); })
.observe(document.getElementById('outline'), { subtree: true, characterData: true, childList: true });
let lastW = 0;
addEventListener('resize', () => { if (stage.clientWidth !== lastW) { lastW = stage.clientWidth; build(true); } });
build(true); lastW = stage.clientWidth;
</script>
</body>
</html>
Getting the map in front of people
You have a mind map. The AI gave it to you as HTML code, though, and a teacher or a team cannot tap branches in a block of code.
The first wall is sharing it. Attach the file and on many phones it opens as code or a download, not a map. To be a page people tap, it has to live somewhere with an address.
In NOS you paste the code into a document and it becomes the map straight away. Choose Create share link and send that link to your teacher, your group or yourself.

The second wall is a renamed branch. Your teacher reads the plan and says one thing: call "The other side" by its proper name, "Counter-arguments".
If you ask the AI again, you wait for a new reply. It often rearranges the layout or the colours while it is at it, so you check the whole map again. Then you have a new file and a new link to send.
In NOS you click "The other side" in the outline on the page and type "Counter-arguments". The link stays the same, and anyone who opens it sees the renamed branch. A typo in a point works the same way.

Colours, line styles and the opening animation live in the code. Change those in the code view, or ask the AI for just that part.
What it does not do
This mind map template is a view of your outline. You cannot drag branches around on it, and it does not save which points were ticked once the page is closed.
For steps in order rather than ideas around a topic, see HTML flowchart. For a page of notes to revise from, see Study notes website.