# Embeddings

How a model turns each token into a list of numbers, so that related meanings end up close together.

Builds on: Tokenization (hard, https://ai.page/c/tokenization/en.md)

Concept id: embeddings. Subject: llms. Licence: CC BY-SA 4.0.

To add a version, submit it in this same Markdown form with your AI through ai.page's connector (submit_version). Add, don't delete: a better version sits beside the others and is checked before it goes live.

## Version 66d84199a48b64f636c6dece722f9cd19ff14724b5820bd591f5dd9fef47bf28

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/66d84199a48b64f636c6dece722f9cd19ff14724b5820bd591f5dd9fef47bf28.md

---
format: 1
concept: "embeddings"
kind: "explanation"
title: "Meaning as a direction"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://en.wikipedia.org/wiki/Word_embedding","https://en.wikipedia.org/wiki/Cosine_similarity"]}
---
A tokenizer turns text into token ids: just numbers in a list, like 403 or 34711. The id says nothing about meaning; 403 isn't "closer" to 404 in any useful way. So the first thing a language model does with each id is look up its **embedding**: a list of numbers, a **vector**, that stands for that token.

```
token "cat"  → id 9246 → [0.12, -0.83, 0.40, …]   (hundreds or thousands of numbers)
```

## Close means related

The vectors aren't chosen by hand. They start random and are **learned** during training, nudged a little at a time so the model predicts text better. Because tokens that appear in similar places in text get used in similar ways, their vectors end up pointing in similar directions: "cat" near "kitten" and "dog", far from "truck".

## Measuring closeness: cosine similarity

Two vectors are compared by the **angle** between them, not their length. The **cosine similarity** is

$$\cos\theta = \frac{a \cdot b}{\lVert a \rVert\,\lVert b \rVert}$$

where $a \cdot b$ multiplies the vectors number by number and adds the results, and $\lVert a \rVert$ is a vector's length. It's **1** when they point the same way, **0** when they're at right angles, and **−1** when they point opposite ways.

## Many dimensions

Real embeddings have hundreds or thousands of numbers, not two. That's room for many kinds of relatedness at once: topic, grammar, tone. No single number means one human idea like "animal" or "royalty"; meanings are spread across many of them, which is why pictures of embeddings squash them down to 2D and lose a lot.

## Where they're used

- **Inside every language model:** embeddings are the model's first layer, and its later layers keep working with vectors like these.
- **Search by meaning:** turn documents and a question into embeddings, then find the documents whose vectors are most similar, even when they share no words with the question.


## Version d12115873874a07825c7b5688b3d1915f809e2edc27b84cee6ffe193a544a23a

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/d12115873874a07825c7b5688b3d1915f809e2edc27b84cee6ffe193a544a23a.md

---
format: 1
concept: "embeddings"
kind: "figure"
title: "Which words point the same way?"
labels: {"lang":"en","level":"beginner","approach":"visual"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":[],"sources":[]}
---
A 2D map with a few words placed as arrows from the centre. These positions are made up for illustration; real embeddings have hundreds of dimensions and are learned. Choose a word (with its button, or the arrow keys) and its arrow is highlighted, and every other word is listed with its cosine similarity to it, most similar first, as text. Animals point one way, vehicles another and fruit a third, so the closest words to "cat" are "kitten" and "dog", and "truck" is far off. You can also drag a word's point to move it, and the list updates.

```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
:root{--paper:#fbfbfd;--ink:#1d1f24;--muted:#6b6f78;--line:#dcdee3;--accent:#3d6fb6;--soft:#e6edf7;color-scheme:light dark}
@media (prefers-color-scheme:dark){:root{--paper:#16181c;--ink:#eceef2;--muted:#9aa0aa;--line:#33363d;--accent:#7aa5e8;--soft:#1f2a3b}}
body{margin:0;font:15px/1.5 system-ui,sans-serif;background:var(--paper);color:var(--ink);padding:14px;display:grid;grid-template-columns:minmax(0,1fr);gap:10px}
@media (min-width:560px){body{grid-template-columns:minmax(0,3fr) minmax(0,2fr)}}
svg{width:100%;height:auto;touch-action:none}
.words{display:flex;flex-wrap:wrap;gap:6px}
button{font:inherit;border:1px solid var(--line);background:var(--paper);color:var(--ink);border-radius:8px;padding:4px 10px;min-height:36px;cursor:pointer}
button[aria-pressed=true]{background:var(--accent);color:var(--paper);border-color:var(--accent)}
ol{margin:0;padding-left:1.4em;font-variant-numeric:tabular-nums}
.muted{color:var(--muted);font-size:13px}
:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
</style>
</head>
<body>
<div>
<svg id="map" viewBox="-1.2 -1.2 2.4 2.4" role="img" aria-label="Words as arrows from the centre"></svg>
<p class="muted">Positions are made up for illustration. Drag a point to move a word.</p>
</div>
<div>
<div class="words" id="words" role="group" aria-label="Choose a word"></div>
<p id="say" aria-live="polite"></p>
<ol id="list"></ol>
</div>
<script>
const pts = { cat: [0.9, 0.8], kitten: [0.95, 0.6], dog: [0.85, 0.95], car: [-0.7, 0.6], truck: [-0.9, 0.5], apple: [0.1, -0.9], banana: [0.3, -0.95] };
const names = Object.keys(pts);
let pick = "cat";
const NS = "http://www.w3.org/2000/svg";
const svg = document.getElementById("map"), list = document.getElementById("list"), say = document.getElementById("say"), words = document.getElementById("words");
const css = (v) => getComputedStyle(document.documentElement).getPropertyValue(v);
const cos = (a, b) => (a[0] * b[0] + a[1] * b[1]) / (Math.hypot(...a) * Math.hypot(...b));
const el = (tag, attrs) => { const e = document.createElementNS(NS, tag); for (const k in attrs) e.setAttribute(k, attrs[k]); return e; };
function draw() {
  svg.textContent = "";
  svg.append(el("line", { x1: -1.1, y1: 0, x2: 1.1, y2: 0, stroke: css("--line"), "stroke-width": 0.01 }), el("line", { x1: 0, y1: -1.1, x2: 0, y2: 1.1, stroke: css("--line"), "stroke-width": 0.01 }));
  for (const w of names) {
    const [x, y] = pts[w], on = w === pick;
    svg.append(el("line", { x1: 0, y1: 0, x2: x, y2: -y, stroke: on ? css("--accent") : css("--muted"), "stroke-width": on ? 0.03 : 0.012 }));
    const dot = el("circle", { cx: x, cy: -y, r: 0.06, fill: on ? css("--accent") : css("--ink"), "data-w": w, style: "cursor:grab" });
    svg.append(dot);
    const t = el("text", { x: x + 0.07, y: -y - 0.06, "font-size": 0.11, fill: css("--ink") });
    t.textContent = w;
    svg.append(t);
  }
  const ranked = names.filter((w) => w !== pick).map((w) => [w, cos(pts[pick], pts[w])]).sort((a, b) => b[1] - a[1]);
  list.innerHTML = ranked.map(([w, c]) => `<li>${w}: ${c.toFixed(2)}</li>`).join("");
  say.textContent = `Closest to "${pick}": ${ranked[0][0]} (${ranked[0][1].toFixed(2)}). Farthest: ${ranked.at(-1)[0]} (${ranked.at(-1)[1].toFixed(2)}).`;
  words.querySelectorAll("button").forEach((b) => b.setAttribute("aria-pressed", String(b.textContent === pick)));
}
for (const w of names) {
  const b = document.createElement("button");
  b.textContent = w;
  b.addEventListener("click", () => { pick = w; draw(); });
  words.append(b);
}
addEventListener("keydown", (e) => {
  if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
  pick = names[(names.indexOf(pick) + (e.key === "ArrowRight" ? 1 : names.length - 1)) % names.length];
  draw();
});
let dragging = null;
const at = (e) => { const p = svg.createSVGPoint(); p.x = e.clientX; p.y = e.clientY; const q = p.matrixTransform(svg.getScreenCTM().inverse()); return [Math.max(-1.1, Math.min(1.1, q.x)), Math.max(-1.1, Math.min(1.1, -q.y))]; };
svg.addEventListener("pointerdown", (e) => { const w = e.target.getAttribute?.("data-w"); if (w) { dragging = w; pick = w; svg.setPointerCapture(e.pointerId); } });
svg.addEventListener("pointermove", (e) => { if (!dragging) return; const p = at(e); if (Math.hypot(...p) > 0.05) { pts[dragging] = p; draw(); } });
svg.addEventListener("pointerup", () => { dragging = null; });
draw();
</script>
</body>
</html>
```

```json
{
  "libraries": [],
  "strings": {}
}
```


## Version 7afc4c1eadb79f833bf45857a80191bf1e72ad015bae7fbb7b18bed6c0275e66

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/7afc4c1eadb79f833bf45857a80191bf1e72ad015bae7fbb7b18bed6c0275e66.md

---
format: 1
concept: "embeddings"
kind: "question"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://en.wikipedia.org/wiki/Word_embedding","https://en.wikipedia.org/wiki/Cosine_similarity"]}
---
What is the cosine similarity of $(5, 12)$ and $(12, 5)$? Give it to two decimal places.

```json
{
  "type": "number",
  "purpose": "measure",
  "answer": 0.71,
  "tolerance": 0.01
}
```


## Version 695bdda481301f4aa205e8887d36362dd830914af318f6ce7a0f7a8bb965b197

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/695bdda481301f4aa205e8887d36362dd830914af318f6ce7a0f7a8bb965b197.md

---
format: 1
concept: "embeddings"
kind: "question"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://en.wikipedia.org/wiki/Word_embedding","https://en.wikipedia.org/wiki/Cosine_similarity"]}
---
Which of these are true? Choose all that are.

```json
{
  "type": "choose",
  "purpose": "measure",
  "options": [
    {
      "text": "Embeddings are learned during training, not written by hand",
      "correct": true
    },
    {
      "text": "Tokens used in similar contexts tend to get vectors pointing in similar directions",
      "correct": true
    },
    {
      "text": "Each number in an embedding stands for one human idea, like \"royalty\"",
      "correct": false,
      "misconception": "dimensions-are-meanings"
    },
    {
      "text": "A token's embedding is the same thing as its id",
      "correct": false,
      "misconception": "embedding-is-id"
    }
  ],
  "multiple": true
}
```


## Version fa58d9f32418563431924fef212909a00674101eedeb61215935906d7000b002

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/fa58d9f32418563431924fef212909a00674101eedeb61215935906d7000b002.md

---
format: 1
concept: "embeddings"
kind: "misconception"
id: "dimensions-are-meanings"
title: "Each number in an embedding means one idea"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://en.wikipedia.org/wiki/Word_embedding","https://en.wikipedia.org/wiki/Cosine_similarity"]}
---
Popular explanations draw embeddings with labelled axes, like "animal" or "royalty", so it's natural to think each number has a meaning you could name. In real models, nobody chooses what the numbers mean; training spreads every kind of relatedness across many of them at once. Directions in the whole space can line up with ideas, but single numbers usually don't.

Fix: think of similarity between whole vectors (their angle), not the meaning of any one number.


## Version cc961c3d4dd795a5558d5b3275841b9ef4a75f16acb40fd6eec6b1e5f4a73119

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/cc961c3d4dd795a5558d5b3275841b9ef4a75f16acb40fd6eec6b1e5f4a73119.md

---
format: 1
concept: "embeddings"
kind: "misconception"
id: "embedding-is-id"
title: "The embedding is the token id"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://en.wikipedia.org/wiki/Word_embedding","https://en.wikipedia.org/wiki/Cosine_similarity"]}
---
Both are numbers attached to a token, so they're easy to mix up. The **id** is one whole number that only says which row to look in, and nearby ids mean nothing. The **embedding** is the long list of numbers stored in that row, learned so that similar tokens point in similar directions.

Fix: id = which row; embedding = what's in the row.


## Version f02749260b4c96c9b5df44d6b0d711c479c90a191869dd1b2987d1a2107ee1e2

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/f02749260b4c96c9b5df44d6b0d711c479c90a191869dd1b2987d1a2107ee1e2.md

---
format: 1
concept: "embeddings"
kind: "question"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://en.wikipedia.org/wiki/Word_embedding","https://en.wikipedia.org/wiki/Cosine_similarity"]}
---
Fill the gaps.

```json
{
  "type": "fill",
  "purpose": "practice",
  "explanation": "Same direction gives 1, right angles give 0, opposite gives −1.",
  "text": "Two vectors pointing the same way have cosine similarity [[1]]. Two pointing in opposite directions have cosine similarity [[2]].",
  "blanks": [
    [
      "1",
      "one",
      "1.0"
    ],
    [
      "-1",
      "−1",
      "minus one",
      "-1.0"
    ]
  ]
}
```


## Version 1487e3231b2d3bb032f1ae392824880d9a8f01b375f9b21ebb50b9a028e42582

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/1487e3231b2d3bb032f1ae392824880d9a8f01b375f9b21ebb50b9a028e42582.md

---
format: 1
concept: "embeddings"
kind: "question"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://en.wikipedia.org/wiki/Word_embedding","https://en.wikipedia.org/wiki/Cosine_similarity"]}
---
Put what happens to text on its way into a language model in order.

```json
{
  "type": "order",
  "purpose": "practice",
  "explanation": "Text → tokens → ids → embeddings → the model.",
  "items": [
    "Cut the text into tokens",
    "Look up each token's id",
    "Look up the embedding vector for each id",
    "Pass the vectors through the model's layers"
  ]
}
```


## Version a1738c9417a2a372f1074d3dda75afacccc42cf94f13910772263d29c6cdf1f1

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/a1738c9417a2a372f1074d3dda75afacccc42cf94f13910772263d29c6cdf1f1.md

---
format: 1
concept: "embeddings"
kind: "question"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://en.wikipedia.org/wiki/Word_embedding","https://en.wikipedia.org/wiki/Cosine_similarity"]}
---
What is the cosine similarity of $(1, 0)$ and $(0, 1)$?

```json
{
  "type": "number",
  "purpose": "practice",
  "hint": "Multiply number by number and add. What do you get?",
  "explanation": "$1 \\times 0 + 0 \\times 1 = 0$, so the cosine is 0: the two vectors are at right angles.",
  "answer": 0,
  "tolerance": 0.001
}
```


## Version 122a7f9e1a2a8a5cc32426ab5a02d0b8b5d8381ea5f133ee5a7deaea5466b02a

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/122a7f9e1a2a8a5cc32426ab5a02d0b8b5d8381ea5f133ee5a7deaea5466b02a.md

---
format: 1
concept: "embeddings"
kind: "question"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://en.wikipedia.org/wiki/Word_embedding","https://en.wikipedia.org/wiki/Cosine_similarity"]}
---
What is a token's embedding?

```json
{
  "type": "choose",
  "purpose": "practice",
  "explanation": "The id only picks a row; the embedding is the learned vector in that row.",
  "options": [
    {
      "text": "A list of numbers, learned in training, that stands for the token so related meanings end up close",
      "correct": true
    },
    {
      "text": "The token's id in the tokenizer's vocabulary",
      "correct": false,
      "misconception": "embedding-is-id"
    },
    {
      "text": "A dictionary definition the model looks up",
      "correct": false
    },
    {
      "text": "A list where each number is one human idea, like \"animal\" or \"size\"",
      "correct": false,
      "misconception": "dimensions-are-meanings"
    }
  ],
  "multiple": false
}
```


## Version 75046dbcbd54d805ea8ae3be8c3110401b3fedec7f3d90ca2004952971627e68

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/75046dbcbd54d805ea8ae3be8c3110401b3fedec7f3d90ca2004952971627e68.md

---
format: 1
concept: "embeddings"
kind: "worked-example"
title: "Three tiny vectors"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://en.wikipedia.org/wiki/Word_embedding","https://en.wikipedia.org/wiki/Cosine_similarity"]}
---
Real embeddings are long, but the sums are the same in 2D. Compare $A = (3, 4)$, $B = (4, 3)$ and $C = (-4, 3)$ with cosine similarity.

```json
{
  "steps": [
    "Lengths first: $\\lVert A \\rVert = \\sqrt{3^2 + 4^2} = 5$, and $B$ and $C$ are also length 5.",
    "$A \\cdot B = 3 \\times 4 + 4 \\times 3 = 24$, so $\\cos = 24 / (5 \\times 5) = 0.96$: nearly the same direction, very similar.",
    "$A \\cdot C = 3 \\times (-4) + 4 \\times 3 = 0$, so $\\cos = 0$: at right angles, unrelated.",
    "$B \\cdot C = 4 \\times (-4) + 3 \\times 3 = -7$, so $\\cos = -7 / 25 = -0.28$: pointing somewhat apart. Of the three, $A$ and $B$ are the closest pair."
  ]
}
```


## Version c2fb41fdb3def498cf2ae07994bd492e2297064f550c7990734f74a1afd00879

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/c2fb41fdb3def498cf2ae07994bd492e2297064f550c7990734f74a1afd00879.md

---
format: 1
concept: "embeddings"
kind: "figure"
title: "Which words point the same way?"
labels: {"lang":"en","level":"beginner","approach":"visual"}
base: "d12115873874a07825c7b5688b3d1915f809e2edc27b84cee6ffe193a544a23a"
changes: ["Uses the ai.page figure kit: the shared design tokens for colour, light and dark, and the shared controls"]
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":[],"sources":[]}
---
A 2D map with a few words placed as arrows from the centre. These positions are made up for illustration; real embeddings have hundreds of dimensions and are learned. Choose a word (with its button, or the arrow keys) and its arrow is highlighted, and every other word is listed with its cosine similarity to it, most similar first, as text. Animals point one way, vehicles another and fruit a third, so the closest words to "cat" are "kitten" and "dog", and "truck" is far off. You can also drag a word's point to move it, and the list updates.

```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body{display:grid;grid-template-columns:minmax(0,1fr);gap:10px}
@media (min-width:560px){body{grid-template-columns:minmax(0,3fr) minmax(0,2fr)}}
svg{width:100%;height:auto;touch-action:none}
.words{display:flex;flex-wrap:wrap;gap:6px}
ol{margin:0;padding-left:1.4em;font-variant-numeric:tabular-nums}
.muted{color:var(--aip-ink-muted);font-size:13px}
</style>
</head>
<body>
<div>
<svg id="map" viewBox="-1.2 -1.2 2.4 2.4" role="img" aria-label="Words as arrows from the centre"></svg>
<p class="muted">Positions are made up for illustration. Drag a point to move a word.</p>
</div>
<div>
<div class="words" id="words" role="group" aria-label="Choose a word"></div>
<p id="say" aria-live="polite"></p>
<ol id="list"></ol>
</div>
<script>
const pts = { cat: [0.9, 0.8], kitten: [0.95, 0.6], dog: [0.85, 0.95], car: [-0.7, 0.6], truck: [-0.9, 0.5], apple: [0.1, -0.9], banana: [0.3, -0.95] };
const names = Object.keys(pts);
let pick = "cat";
const NS = "http://www.w3.org/2000/svg";
const svg = document.getElementById("map"), list = document.getElementById("list"), say = document.getElementById("say"), words = document.getElementById("words");
const css = (v) => getComputedStyle(document.documentElement).getPropertyValue(v);
const cos = (a, b) => (a[0] * b[0] + a[1] * b[1]) / (Math.hypot(...a) * Math.hypot(...b));
const el = (tag, attrs) => { const e = document.createElementNS(NS, tag); for (const k in attrs) e.setAttribute(k, attrs[k]); return e; };
function draw() {
  svg.textContent = "";
  svg.append(el("line", { x1: -1.1, y1: 0, x2: 1.1, y2: 0, stroke: css("--aip-line"), "stroke-width": 0.01 }), el("line", { x1: 0, y1: -1.1, x2: 0, y2: 1.1, stroke: css("--aip-line"), "stroke-width": 0.01 }));
  for (const w of names) {
    const [x, y] = pts[w], on = w === pick;
    svg.append(el("line", { x1: 0, y1: 0, x2: x, y2: -y, stroke: on ? css("--aip-accent") : css("--aip-ink-muted"), "stroke-width": on ? 0.03 : 0.012 }));
    const dot = el("circle", { cx: x, cy: -y, r: 0.06, fill: on ? css("--aip-accent") : css("--aip-ink"), "data-w": w, style: "cursor:grab" });
    svg.append(dot);
    const t = el("text", { x: x + 0.07, y: -y - 0.06, "font-size": 0.11, fill: css("--aip-ink") });
    t.textContent = w;
    svg.append(t);
  }
  const ranked = names.filter((w) => w !== pick).map((w) => [w, cos(pts[pick], pts[w])]).sort((a, b) => b[1] - a[1]);
  list.innerHTML = ranked.map(([w, c]) => `<li>${w}: ${c.toFixed(2)}</li>`).join("");
  say.textContent = `Closest to "${pick}": ${ranked[0][0]} (${ranked[0][1].toFixed(2)}). Farthest: ${ranked.at(-1)[0]} (${ranked.at(-1)[1].toFixed(2)}).`;
  words.querySelectorAll("button").forEach((b) => b.setAttribute("aria-pressed", String(b.textContent === pick)));
}
for (const w of names) {
  const b = document.createElement("button");
  b.textContent = w;
  b.addEventListener("click", () => { pick = w; draw(); });
  words.append(b);
}
addEventListener("keydown", (e) => {
  if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
  pick = names[(names.indexOf(pick) + (e.key === "ArrowRight" ? 1 : names.length - 1)) % names.length];
  draw();
});
let dragging = null;
const at = (e) => { const p = svg.createSVGPoint(); p.x = e.clientX; p.y = e.clientY; const q = p.matrixTransform(svg.getScreenCTM().inverse()); return [Math.max(-1.1, Math.min(1.1, q.x)), Math.max(-1.1, Math.min(1.1, -q.y))]; };
svg.addEventListener("pointerdown", (e) => { const w = e.target.getAttribute?.("data-w"); if (w) { dragging = w; pick = w; svg.setPointerCapture(e.pointerId); } });
svg.addEventListener("pointermove", (e) => { if (!dragging) return; const p = at(e); if (Math.hypot(...p) > 0.05) { pts[dragging] = p; draw(); } });
svg.addEventListener("pointerup", () => { dragging = null; });
draw();
</script>
</body>
</html>
```

```json
{
  "libraries": [],
  "strings": {}
}
```

