0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously
Two boxes of exactly the same size, four identical right triangles going into each — so whatever space is left over has to match, and that single observation is the whole proof of a² + b² = c². The left box opens already assembled, leaving two square holes; drag the four spare triangles into the corners of the right box and the leftover space collapses into one tilted square, at which point the equation earns its green. Then pull the shape slider: every number, both holes and the tilt angle re-derive live, and the proof never breaks — which is rather the point. Tap or Tab plus Enter places pieces too, if you would rather not drag.
Source — the code you see is the code that just ran 842 lines 37.6 KB index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>A Proof You Can Drag</title>
<style>
/* sloprun design tokens — inline this block into every demo (self-contained rule).
Identity: instrument-panel. Machine facts in mono; human words in sans.
Green is EARNED: only for "it ran / it worked" states, never decoration. */
:root {
--bg: #F6F6F3; /* neutral paper, not cream */
--surface: #FFFFFF;
--ink: #1A1D21;
--muted: #5A6068;
--line: #E3E2DC;
--accent: #C05600; /* phosphor amber, darkened for light bg */
--accent-soft: #FFF3E6;
--run: #1A7F37; /* earned green */
--run-soft: #E7F4EA;
--danger: #C93C37;
--radius: 10px;
--font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
}
:root[data-theme="dark"] {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }
/* --- demo-local supplements (derived; same three theme states) --- */
:root { --canvas: #FBFBF8; --track: #ECEBE5; --shadow: 0 1px 2px rgba(20,22,26,.05); }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { --canvas: #171A20; --track: #23272F; --shadow: 0 1px 2px rgba(0,0,0,.32); }
}
:root[data-theme="dark"] { --canvas: #171A20; --track: #23272F; --shadow: 0 1px 2px rgba(0,0,0,.32); }
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { -webkit-text-size-adjust: 100%; line-height: 1.45; overflow-x: hidden; }
.wrap { max-width: 1000px; margin: 0 auto; padding: 22px 16px 30px; }
header .chip {
display: inline-block; font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .06em;
text-transform: uppercase; color: var(--accent); background: var(--accent-soft);
border: 1px solid var(--line); border-radius: 999px; padding: 3px 9px; margin-bottom: 10px;
}
header h1 { font-size: 21px; margin: 0 0 4px; letter-spacing: -.01em; }
header p.lede { margin: 0; color: var(--muted); font-size: 14.5px; max-width: 66ch; }
.panel {
margin-top: 14px; background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden;
}
.phead {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
padding: 9px 12px; border-bottom: 1px solid var(--line);
}
.phead h2 {
margin: 0; font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .08em;
text-transform: uppercase; color: var(--muted); font-weight: 500;
}
.phead .spacer { flex: 1 1 auto; }
.pill {
font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .05em; padding: 3px 9px;
border-radius: 999px; border: 1px solid var(--line); color: var(--muted); background: var(--bg);
white-space: nowrap;
}
.pill.ok { color: var(--run); background: var(--run-soft); border-color: var(--run); }
/* controls */
.controls { display: flex; flex-wrap: wrap; gap: 8px 10px; align-items: center; padding: 11px 12px; }
.controls.sep { border-top: 1px solid var(--line); }
.field { display: flex; align-items: center; gap: 8px; flex: 1 1 260px; min-width: 220px; }
.flab { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); letter-spacing: .06em; text-transform: uppercase; white-space: nowrap; }
input[type=range] { flex: 1 1 auto; min-width: 90px; accent-color: var(--accent); height: 26px; }
.rd { font-family: var(--font-mono); font-size: 12px; color: var(--ink); font-variant-numeric: tabular-nums; white-space: nowrap; }
button {
font-family: var(--font-sans); font-size: 13px; color: var(--ink); background: var(--surface);
border: 1px solid var(--line); border-radius: 8px; padding: 7px 11px; min-height: 36px;
cursor: pointer; white-space: nowrap;
}
button:hover:not(:disabled) { border-color: var(--muted); }
button.mono { font-family: var(--font-mono); font-size: 12px; letter-spacing: .04em; }
button[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); font-weight: 600; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 8px; }
/* canvas */
.canvas { background: var(--canvas); border-top: 1px solid var(--line); padding: 4px 8px 10px; }
svg#board { display: block; width: 100%; height: auto; margin: 0 auto; user-select: none; -webkit-user-select: none; }
/* The stacked layout (JS switches to it at 560px) is a tall, narrow viewBox — cap it
so a 500px-wide window does not blow the board up to 1600px of vertical scroll. */
@media (max-width: 560px) { svg#board { max-width: 400px; } }
@media (min-width: 721px) { svg#board { max-width: 880px; } }
#gBoards, #gGhosts, #gStage { pointer-events: none; }
.box { fill: var(--surface); stroke: var(--line); stroke-width: 1.6; }
.boxlab { font-family: var(--font-mono); font-size: 10px; letter-spacing: .09em; text-transform: uppercase; fill: var(--muted); }
.boxnum { font-family: var(--font-mono); font-size: 10px; letter-spacing: .06em; fill: var(--muted); }
.hole { fill: var(--ink); fill-opacity: .055; stroke: var(--ink); stroke-opacity: .34; stroke-width: 1.6; }
.holeLab { font-family: var(--font-mono); font-size: 11.5px; fill: var(--ink); fill-opacity: .75; }
.edgeLab { font-family: var(--font-mono); font-size: 11px; fill: var(--ink); fill-opacity: .6; }
#gBoards.proved .hole { fill: var(--run); fill-opacity: .17; stroke: var(--run); stroke-opacity: 1; stroke-width: 2; }
#gBoards.proved .holeLab, #gBoards.proved .edgeLab { fill: var(--run); fill-opacity: 1; }
.ghost { fill: none; stroke: var(--muted); stroke-opacity: .5; stroke-width: 1.5; stroke-dasharray: 6 5; stroke-linejoin: round; }
.ghost.cand { stroke: var(--accent); stroke-opacity: 1; stroke-width: 2.6; stroke-dasharray: none; }
.ghost.ok { stroke: var(--accent); stroke-opacity: .85; stroke-width: 2.1; stroke-dasharray: 3 4; }
.ghost.next { stroke: var(--accent); stroke-opacity: 1; stroke-width: 2.2; animation: pulse 1.7s ease-in-out infinite; }
@keyframes pulse { 0%,100% { stroke-opacity: .3 } 50% { stroke-opacity: 1 } }
@media (prefers-reduced-motion: reduce) {
.ghost.next { animation: none; stroke-opacity: 1; stroke-width: 2.6; stroke-dasharray: none; }
.piece.dragging { filter: none; }
* { transition-duration: 0s !important; animation-duration: 0s !important; animation-iteration-count: 1 !important; }
}
.cell { fill: none; stroke: var(--line); stroke-width: 1.4; stroke-dasharray: 4 5; rx: 8; }
.stagelab { font-family: var(--font-mono); font-size: 10px; letter-spacing: .09em; text-transform: uppercase; fill: var(--muted); }
.emptylab { font-family: var(--font-mono); font-size: 12px; letter-spacing: .07em; text-transform: uppercase; fill: var(--run); }
.piece { cursor: grab; touch-action: none; }
.piece.dragging { cursor: grabbing; }
.hit { fill: transparent; stroke: transparent; stroke-width: 18; stroke-linejoin: round; pointer-events: all; }
.tri { fill: var(--accent); fill-opacity: .18; stroke: var(--accent); stroke-width: 1.7; stroke-linejoin: round; pointer-events: none; }
.ra { fill: none; stroke: var(--accent); stroke-opacity: .65; stroke-width: 1.3; pointer-events: none; }
.plab { font-family: var(--font-mono); font-size: 10px; fill: var(--accent); fill-opacity: .8; pointer-events: none; }
.piece:hover .tri { fill-opacity: .3; }
.piece:focus { outline: none; }
.piece:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.piece:focus-visible .tri { stroke-width: 3.2; fill-opacity: .32; }
.piece.dragging .tri { fill-opacity: .34; stroke-width: 2.4; }
.piece.dragging { filter: drop-shadow(0 3px 7px rgba(0,0,0,.28)); }
.tri.miss { stroke: var(--danger); fill: var(--danger); fill-opacity: .26; }
/* verdict */
.verdict { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 12px; border-top: 1px solid var(--line); background: var(--bg); }
.eq { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.term { display: inline-flex; align-items: baseline; gap: 6px; padding: 3px 9px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); }
.term i { font-style: normal; font-size: 11px; color: var(--muted); letter-spacing: .04em; }
.term b { font-weight: 600; font-size: 15px; }
.op { font-size: 15px; color: var(--muted); }
.rel { font-size: 15px; padding: 3px 10px; border-radius: 8px; border: 1px dashed var(--line); color: var(--muted); }
.verdict.proved .rel { border-style: solid; border-color: var(--run); color: var(--run); background: var(--run-soft); font-weight: 700; }
.verdict.proved .term b { color: var(--run); }
.verdict .spacer { flex: 1 1 auto; }
/* stats */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(92px, 1fr)); gap: 1px; background: var(--line); border-top: 1px solid var(--line); }
.stat { background: var(--surface); padding: 8px 11px; }
.stat .lab { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.stat .val { font-family: var(--font-mono); font-size: 15.5px; font-variant-numeric: tabular-nums; line-height: 1.35; }
/* steps */
.steps { list-style: none; margin: 0; padding: 4px 0 6px; }
.steps li { display: flex; gap: 10px; align-items: flex-start; padding: 8px 12px; border-left: 3px solid transparent; }
.steps .num { font-family: var(--font-mono); font-size: 11px; width: 20px; height: 20px; flex: 0 0 auto; border-radius: 50%; border: 1px solid var(--line); display: flex; align-items: center; justify-content: center; color: var(--muted); background: var(--bg); margin-top: 1px; }
.steps .txt { font-size: 13.5px; color: var(--muted); }
.steps li.now { border-left-color: var(--accent); background: var(--accent-soft); }
.steps li.now .num { border-color: var(--accent); color: var(--accent); font-weight: 700; }
.steps li.now .txt { color: var(--ink); }
.steps li.done .num { border-color: var(--run); color: var(--run); background: var(--run-soft); }
.steps li.done .txt { color: var(--muted); }
.hint { margin: 12px 2px 0; font-size: 12.5px; color: var(--muted); }
.hint kbd { font-family: var(--font-mono); font-size: 11px; border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 5px; padding: 1px 5px; background: var(--surface); color: var(--ink); }
footer { margin-top: 16px; font-family: var(--font-mono); font-size: 11px; color: var(--muted); letter-spacing: .04em; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
@media (max-width: 520px) {
.field { flex: 1 1 100%; }
.stat .val { font-size: 14px; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<span class="chip">geometry · drag to prove</span>
<h1>A proof you can drag</h1>
<p class="lede">Two boxes of exactly the same size. Four identical right triangles go in each. Whatever
space is left over has to be equal — and the left box leaves two squares while the right box leaves one.
That is the whole proof of <span class="rd">a² + b² = c²</span>. Fill the right box and watch it land.</p>
</header>
<div class="panel">
<div class="phead">
<h2>rearrangement board</h2>
<span class="spacer"></span>
<span class="pill" id="pcount">placed 4 / 8</span>
</div>
<div class="controls">
<div class="field">
<span class="flab">shape</span>
<input type="range" id="shape" min="0.6" max="6.4" step="0.1" value="3"
aria-label="Triangle shape: length of side a, with a plus b fixed at 7">
<span class="rd" id="shaperd">a 3.00 · b 4.00</span>
</div>
<button class="mono" id="p345" title="Set the classic 3-4-5 triangle">3 · 4 · 5</button>
<button id="solve">Place them for me</button>
<button id="empty">Empty both boxes</button>
<button id="reset">Reset</button>
<button id="guide" aria-pressed="true" aria-controls="stepsWrap">Guided steps</button>
</div>
<div id="stepsWrap">
<ol class="steps" id="steps"></ol>
</div>
<div class="canvas">
<svg id="board" viewBox="0 0 636 474" preserveAspectRatio="xMidYMid meet" role="group"
aria-label="Two same-size boxes to fill with four right triangles each">
<g id="gBoards"></g>
<g id="gGhosts"></g>
<g id="gStage"></g>
<g id="gPieces"></g>
</svg>
</div>
<div class="verdict" id="verdict">
<div class="eq">
<span class="term"><i>a²</i><b id="ea">9.00</b></span>
<span class="op">+</span>
<span class="term"><i>b²</i><b id="eb">16.00</b></span>
<span class="rel" id="erel">=?</span>
<span class="term"><i>c²</i><b id="ec">25.00</b></span>
</div>
<span class="spacer"></span>
<span class="pill" id="vpill">fill both boxes to show why</span>
</div>
<div class="stats">
<div class="stat"><div class="lab">side a</div><div class="val" id="sa">3.00</div></div>
<div class="stat"><div class="lab">side b</div><div class="val" id="sb">4.00</div></div>
<div class="stat"><div class="lab">slope side c</div><div class="val" id="sc">5.00</div></div>
<div class="stat"><div class="lab">4 triangles</div><div class="val" id="st">24.00</div></div>
<div class="stat"><div class="lab">tilt</div><div class="val" id="sd">53.13°</div></div>
<div class="stat"><div class="lab">whole numbers</div><div class="val" id="sw">yes</div></div>
</div>
</div>
<p class="hint">Drag a triangle onto a dashed outline and it snaps in — or just tap one and it flies to the
next outline that fits. Keyboard: <kbd>Tab</kbd> to a triangle, <kbd>Enter</kbd> to place it or take it back
out. A piece only fits an outline facing the same way.</p>
<p class="sr" id="sr" aria-live="polite" role="status"></p>
<footer>demo · sloprun.dev</footer>
</div>
<script>
(function () {
"use strict";
/* ---------- theme bridge (platform postMessage + standalone media query) ---------- */
window.addEventListener("message", function (e) {
var d = e && e.data;
if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
document.documentElement.setAttribute("data-theme", d.theme);
}
});
var $ = function (id) { return document.getElementById(id); };
var NS = "http://www.w3.org/2000/svg";
var mqR = window.matchMedia ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
var mqN = window.matchMedia ? window.matchMedia("(max-width: 560px)") : { matches: false, addEventListener: function(){} };
function reduce() { return !!(mqR && mqR.matches); }
/* ---------- geometry constants ---------- */
var U = 40; /* pixels per unit */
var S = 7; /* box side, in units — a + b is always 7 */
var BOX = S * U; /* 280 */
var PAD = 16, GAP = 44, SNAP = 34;
var ROTS = [0, 90, 180, 270];
var BOARDNAME = ["left", "right"];
/* Slot i of every board holds a triangle turned by ROTS[i].
Left box (two square holes): the four triangles pair up into two a-by-b rectangles.
Right box (one tilted hole): the four triangles sit in the four corners. */
function slotPos(board, i, a, b) {
if (board === 0) {
if (i === 0) return { x: b, y: 0 };
if (i === 1) return { x: b, y: b };
if (i === 2) return { x: S, y: b };
return { x: 0, y: S };
}
if (i === 0) return { x: 0, y: 0 };
if (i === 1) return { x: S, y: 0 };
if (i === 2) return { x: S, y: S };
return { x: 0, y: S };
}
/* ---------- state ---------- */
var st = { a: 3, guide: true, pieces: [], drag: null, cand: null };
for (var i = 0; i < 8; i++) {
st.pieces.push({
id: i, rot: ROTS[i % 4], slot: null,
cur: { x: 0, y: 0, k: 1 }, el: null, missT: 0
});
}
function initialPlacement() {
for (var i = 0; i < 8; i++) st.pieces[i].slot = (i < 4) ? { b: 0, i: i } : null;
}
initialPlacement();
function sideA() { return st.a; }
function sideB() { return S - st.a; }
function pieceAt(b, i) {
for (var n = 0; n < 8; n++) { var p = st.pieces[n]; if (p.slot && p.slot.b === b && p.slot.i === i) return p; }
return null;
}
function boardFull(b) { return !!(pieceAt(b, 0) && pieceAt(b, 1) && pieceAt(b, 2) && pieceAt(b, 3)); }
function proved() { return boardFull(0) && boardFull(1); }
function placedCount() { var n = 0; for (var i = 0; i < 8; i++) if (st.pieces[i].slot) n++; return n; }
function stageList() { var o = []; for (var i = 0; i < 8; i++) if (!st.pieces[i].slot) o.push(st.pieces[i]); return o; }
/* ---------- layout ---------- */
var L = {};
function computeLayout() {
var stacked = mqN.matches;
L.stacked = stacked;
L.top = 34;
if (!stacked) {
L.W = PAD * 2 + BOX * 2 + GAP;
L.boards = [{ x: PAD, y: L.top }, { x: PAD + BOX + GAP, y: L.top }];
L.stageY = L.top + BOX + 36;
L.cols = 8; L.cellH = 94;
} else {
L.W = PAD * 2 + BOX;
L.boards = [{ x: PAD, y: L.top }, { x: PAD, y: L.top + BOX + 44 }];
L.stageY = L.top + BOX * 2 + 44 + 36;
L.cols = 4; L.cellH = 92;
}
L.stageX = PAD;
L.cellW = (L.W - PAD * 2) / L.cols;
var ns = stageList().length;
L.rows = Math.max(1, Math.ceil(ns / L.cols));
L.bandH = ns ? L.cellH * L.rows : 46;
L.H = L.stageY + L.bandH + 20;
}
function stageScale() {
var maxLeg = Math.max(sideA(), sideB()) * U;
return Math.min((L.cellW - 16) / maxLeg, (L.cellH - 16) / maxLeg, 0.8);
}
function cellRect(idx, count) {
var row = Math.floor(idx / L.cols), col = idx - row * L.cols;
var inRow = Math.min(L.cols, count - row * L.cols);
var startX = L.stageX + ((L.W - PAD * 2) - inRow * L.cellW) / 2;
return { x: startX + col * L.cellW, y: L.stageY + row * L.cellH };
}
function cellCenter(idx, count) {
var r = cellRect(idx, count);
return { x: r.x + L.cellW / 2, y: r.y + L.cellH / 2 };
}
function bboxCenter(rot) {
var a = sideA(), b = sideB();
if (rot === 0) return [a / 2, b / 2];
if (rot === 90) return [-b / 2, a / 2];
if (rot === 180) return [-a / 2, -b / 2];
return [b / 2, -a / 2];
}
function targetOf(p) {
if (p.slot) {
var bd = L.boards[p.slot.b], sp = slotPos(p.slot.b, p.slot.i, sideA(), sideB());
return { x: bd.x + sp.x * U, y: bd.y + sp.y * U, k: 1 };
}
var list = stageList(), idx = list.indexOf(p), k = stageScale();
if (idx < 0) idx = 0;
var c = cellCenter(idx, list.length), bc = bboxCenter(p.rot);
return { x: c.x - bc[0] * U * k, y: c.y - bc[1] * U * k, k: k };
}
/* rotate local unit-coords into absolute svg coords */
function mk(px, py, rot, k) {
var r = rot * Math.PI / 180, c = Math.cos(r), s = Math.sin(r);
return function (lx, ly) {
var X = lx * U * k, Y = ly * U * k;
return [px + X * c - Y * s, py + X * s + Y * c];
};
}
function pts(arr) {
var o = "";
for (var i = 0; i < arr.length; i++) o += (i ? " " : "") + arr[i][0].toFixed(2) + "," + arr[i][1].toFixed(2);
return o;
}
function el(tag, cls, parent) {
var e = document.createElementNS(NS, tag);
if (cls) e.setAttribute("class", cls);
if (parent) parent.appendChild(e);
return e;
}
/* ---------- build DOM once ---------- */
var svg = $("board"), gB = $("gBoards"), gG = $("gGhosts"), gS = $("gStage"), gP = $("gPieces");
var boardEls = [], ghostEls = [], cellEls = [], stageLab, emptyLab;
for (var b = 0; b < 2; b++) {
var g = el("g", null, gB);
var o = {
rect: el("rect", "box", g),
title: el("text", "boxlab", g),
side: el("text", "boxnum", g),
holes: [el("rect", "hole", g), el("rect", "hole", g)],
poly: el("polygon", "hole", g),
labs: [el("text", "holeLab", g), el("text", "holeLab", g)],
cLab: el("text", "edgeLab", g)
};
o.side.setAttribute("text-anchor", "end");
for (var q = 0; q < 2; q++) { o.labs[q].setAttribute("text-anchor", "middle"); o.labs[q].setAttribute("dominant-baseline", "central"); }
o.cLab.setAttribute("text-anchor", "middle"); o.cLab.setAttribute("dominant-baseline", "central");
boardEls.push(o);
}
for (var b2 = 0; b2 < 2; b2++) for (var i2 = 0; i2 < 4; i2++) ghostEls.push(el("polygon", "ghost", gG));
for (var c2 = 0; c2 < 8; c2++) { var r = el("rect", "cell", gS); r.setAttribute("rx", "8"); cellEls.push(r); }
stageLab = el("text", "stagelab", gS);
stageLab.textContent = "spare pieces";
emptyLab = el("text", "emptylab", gS);
emptyLab.setAttribute("text-anchor", "middle");
emptyLab.setAttribute("dominant-baseline", "central");
emptyLab.style.display = "none";
for (var n = 0; n < 8; n++) {
var p = st.pieces[n];
var pg = el("g", "piece", gP);
pg.setAttribute("tabindex", "0");
pg.setAttribute("role", "button");
p.el = {
g: pg,
hit: el("polygon", "hit", pg),
tri: el("polygon", "tri", pg),
ra: el("path", "ra", pg),
la: el("text", "plab", pg),
lb: el("text", "plab", pg)
};
p.el.la.textContent = "a"; p.el.lb.textContent = "b";
p.el.la.setAttribute("text-anchor", "middle"); p.el.la.setAttribute("dominant-baseline", "central");
p.el.lb.setAttribute("text-anchor", "middle"); p.el.lb.setAttribute("dominant-baseline", "central");
bindPiece(p);
}
/* ---------- drawing ---------- */
function drawBoards() {
var a = sideA(), b = sideB(), full = [boardFull(0), boardFull(1)];
gB.setAttribute("class", proved() ? "proved" : "");
for (var bi = 0; bi < 2; bi++) {
var o = boardEls[bi], bd = L.boards[bi];
o.rect.setAttribute("x", bd.x); o.rect.setAttribute("y", bd.y);
o.rect.setAttribute("width", BOX); o.rect.setAttribute("height", BOX);
o.rect.setAttribute("rx", "3");
o.title.setAttribute("x", bd.x); o.title.setAttribute("y", bd.y - 12);
o.title.textContent = bi === 0 ? "left box · two square holes" : "right box · one tilted hole";
o.side.setAttribute("x", bd.x + BOX); o.side.setAttribute("y", bd.y - 12);
o.side.textContent = "side " + S.toFixed(2);
var show = full[bi];
if (bi === 0) {
o.poly.setAttribute("points", "");
o.poly.style.display = "none";
o.cLab.style.display = "none";
var sq = [{ x: 0, y: 0, s: b, nm: "b" }, { x: b, y: b, s: a, nm: "a" }];
for (var h = 0; h < 2; h++) {
var R = o.holes[h], d = sq[h];
R.style.display = show ? "" : "none";
R.setAttribute("x", bd.x + d.x * U); R.setAttribute("y", bd.y + d.y * U);
R.setAttribute("width", Math.max(0, d.s * U)); R.setAttribute("height", Math.max(0, d.s * U));
var T = o.labs[h];
var cx = bd.x + (d.x + d.s / 2) * U, cy = bd.y + (d.y + d.s / 2) * U;
T.setAttribute("x", cx); T.setAttribute("y", cy);
var side = d.s * U;
T.style.display = (show && side > 30) ? "" : "none";
T.textContent = side > 78 ? (d.nm + "² = " + (d.s * d.s).toFixed(2)) : (d.nm + "²");
}
} else {
o.holes[0].style.display = "none"; o.holes[1].style.display = "none";
o.labs[1].style.display = "none";
o.poly.style.display = show ? "" : "none";
var P = [[bd.x + a * U, bd.y], [bd.x + S * U, bd.y + a * U], [bd.x + b * U, bd.y + S * U], [bd.x, bd.y + b * U]];
o.poly.setAttribute("points", pts(P));
var cc = Math.hypot(a, b);
var T2 = o.labs[0], mx = bd.x + BOX / 2, my = bd.y + BOX / 2;
T2.setAttribute("x", mx); T2.setAttribute("y", my);
T2.style.display = show ? "" : "none";
T2.textContent = "c² = " + (cc * cc).toFixed(2);
/* small c on one slanted edge, nudged toward the middle */
var ex = (P[0][0] + P[1][0]) / 2, ey = (P[0][1] + P[1][1]) / 2;
var dx = mx - ex, dy = my - ey, dl = Math.hypot(dx, dy) || 1;
o.cLab.setAttribute("x", ex + dx / dl * 14); o.cLab.setAttribute("y", ey + dy / dl * 14);
o.cLab.textContent = "c";
o.cLab.style.display = (show && cc * U > 60) ? "" : "none";
}
}
}
function nextTargetSlot() {
var b = boardFull(0) ? 1 : 0;
if (boardFull(0) && boardFull(1)) return null;
for (var i = 0; i < 4; i++) if (!pieceAt(b, i)) return { b: b, i: i };
return null;
}
function drawGhosts() {
var a = sideA(), bb = sideB(), nt = st.guide ? nextTargetSlot() : null;
var held = st.drag ? st.drag.p : null;
for (var b = 0; b < 2; b++) for (var i = 0; i < 4; i++) {
var e = ghostEls[b * 4 + i], occupied = !!pieceAt(b, i);
if (occupied) { e.style.display = "none"; e.setAttribute("class", "ghost"); continue; }
e.style.display = "";
var bd = L.boards[b], sp = slotPos(b, i, a, bb), T = mk(bd.x + sp.x * U, bd.y + sp.y * U, ROTS[i], 1);
e.setAttribute("points", pts([T(0, 0), T(a, 0), T(0, bb)]));
var cls = "ghost";
if (st.cand && st.cand.b === b && st.cand.i === i) cls += " cand";
else if (nt && nt.b === b && nt.i === i && !held) cls += " next";
else if (held && held.rot === ROTS[i]) cls += " ok"; /* every outline this piece could fit */
e.setAttribute("class", cls);
}
}
function drawStage() {
var list = stageList(), n = list.length;
for (var i = 0; i < 8; i++) {
var e = cellEls[i];
if (i >= n) { e.style.display = "none"; continue; }
e.style.display = "";
var r = cellRect(i, n);
e.setAttribute("x", r.x + 3);
e.setAttribute("y", r.y + 3);
e.setAttribute("width", L.cellW - 6);
e.setAttribute("height", L.cellH - 6);
}
stageLab.setAttribute("x", L.stageX);
stageLab.setAttribute("y", L.stageY - 9);
stageLab.style.display = n ? "" : "none";
stageLab.textContent = "spare pieces · " + n;
emptyLab.style.display = n ? "none" : "";
emptyLab.setAttribute("x", L.W / 2);
emptyLab.setAttribute("y", L.stageY + L.bandH / 2);
emptyLab.textContent = "all eight placed · both boxes full";
}
function drawPiece(p) {
var a = sideA(), b = sideB(), k = p.cur.k;
var T = mk(p.cur.x, p.cur.y, p.rot, k);
var O = T(0, 0), A = T(a, 0), B = T(0, b);
var P = pts([O, A, B]);
p.el.tri.setAttribute("points", P);
p.el.hit.setAttribute("points", P);
p.el.hit.setAttribute("stroke-width", 18);
var minLeg = Math.min(a, b) * U * k;
if (minLeg > 34) {
var m = 9 / (U * k), c1 = T(m, 0), c2 = T(m, m), c3 = T(0, m);
p.el.ra.setAttribute("d", "M" + c1[0].toFixed(2) + "," + c1[1].toFixed(2) +
"L" + c2[0].toFixed(2) + "," + c2[1].toFixed(2) + "L" + c3[0].toFixed(2) + "," + c3[1].toFixed(2));
p.el.ra.style.display = "";
} else p.el.ra.style.display = "none";
var r = p.rot * Math.PI / 180, cs = Math.cos(r), sn = Math.sin(r);
var thin = minLeg < 46;
if (a * U * k > 34 && !thin) {
var la = T(a / 2, 0);
p.el.la.setAttribute("x", la[0] + (-sn) * 11); p.el.la.setAttribute("y", la[1] + cs * 11);
p.el.la.style.display = "";
} else p.el.la.style.display = "none";
if (b * U * k > 34 && !thin) {
var lb = T(0, b / 2);
p.el.lb.setAttribute("x", lb[0] + cs * 11); p.el.lb.setAttribute("y", lb[1] + sn * 11);
p.el.lb.style.display = "";
} else p.el.lb.style.display = "none";
}
/* labels are set straight from render(), not from the animation frame, so assistive
tech never reads a stale "in the spare rack" after a piece has just been placed */
function labelPieces() {
for (var i = 0; i < 8; i++) {
var p = st.pieces[i];
var where = p.slot ? ("placed in the " + BOARDNAME[p.slot.b] + " box") : "in the spare rack";
p.el.g.setAttribute("aria-label", "Triangle " + (p.id + 1) + ", " + where +
". Press Enter to " + (p.slot ? "take it out" : "place it") + ".");
}
}
/* ---------- animation loop ---------- */
var running = false;
function kick() { if (!running) { running = true; requestAnimationFrame(tick); } }
function tick() {
var moving = false, now = Date.now();
for (var i = 0; i < 8; i++) {
var p = st.pieces[i];
if (st.drag && st.drag.p === p) { moving = true; drawPiece(p); continue; }
var t = targetOf(p);
if (reduce()) { p.cur.x = t.x; p.cur.y = t.y; p.cur.k = t.k; }
else {
p.cur.x += (t.x - p.cur.x) * 0.3;
p.cur.y += (t.y - p.cur.y) * 0.3;
p.cur.k += (t.k - p.cur.k) * 0.3;
if (Math.abs(t.x - p.cur.x) + Math.abs(t.y - p.cur.y) > 0.35 || Math.abs(t.k - p.cur.k) > 0.002) moving = true;
else { p.cur.x = t.x; p.cur.y = t.y; p.cur.k = t.k; }
}
if (p.missT && now > p.missT) { p.missT = 0; p.el.tri.setAttribute("class", "tri"); }
if (p.missT) moving = true;
drawPiece(p);
}
if (moving) requestAnimationFrame(tick); else running = false;
}
/* ---------- readouts ---------- */
function say(msg) { $("sr").textContent = msg; }
function readouts() {
var a = sideA(), b = sideB(), c = Math.hypot(a, b);
$("shaperd").textContent = "a " + a.toFixed(2) + " · b " + b.toFixed(2);
$("shape").setAttribute("aria-valuetext",
"short side a " + a.toFixed(2) + ", long side b " + b.toFixed(2) + ", slope side c " + c.toFixed(2));
$("ea").textContent = (a * a).toFixed(2);
$("eb").textContent = (b * b).toFixed(2);
$("ec").textContent = (c * c).toFixed(2);
$("sa").textContent = a.toFixed(2);
$("sb").textContent = b.toFixed(2);
$("sc").textContent = c.toFixed(2);
$("st").textContent = (2 * a * b).toFixed(2);
$("sd").textContent = (Math.atan2(b, a) * 180 / Math.PI).toFixed(2) + "°";
var whole = Math.abs(a - Math.round(a)) < 1e-9 && Math.abs(c - Math.round(c)) < 1e-9;
$("sw").textContent = whole ? (Math.round(Math.min(a, b)) + "·" + Math.round(Math.max(a, b)) + "·" + Math.round(c)) : "no";
var np = placedCount(), ok = proved();
$("pcount").textContent = "placed " + np + " / 8";
$("pcount").className = "pill" + (ok ? " ok" : "");
var v = $("verdict"), pill = $("vpill");
v.className = "verdict" + (ok ? " proved" : "");
$("erel").textContent = ok ? "=" : "=?";
if (ok) {
pill.className = "pill ok";
pill.textContent = "holes match · both " + (a * a + b * b).toFixed(2);
} else {
pill.className = "pill";
pill.textContent = boardFull(0) ? "now fill the right box" : "fill both boxes to show why";
}
drawSteps();
}
var STEPS = [
"Two boxes, exactly the same size: each side is a + b.",
"The left box holds four triangles and leaves two square holes — one a across, one b across.",
"The right box holds the same four triangles, turned into the corners. It leaves one tilted hole, c across.",
"Same box, same four triangles. The leftover space has to be equal — so a² + b² = c²."
];
function drawSteps() {
var wrap = $("stepsWrap");
wrap.hidden = !st.guide;
if (!st.guide) return;
var done = [true, boardFull(0), boardFull(1), proved()];
var cur = 0;
while (cur < 4 && done[cur]) cur++;
var ol = $("steps");
if (ol.childElementCount !== 4) {
ol.textContent = "";
for (var i = 0; i < 4; i++) {
var li = document.createElement("li");
var nm = document.createElement("span"); nm.className = "num";
var tx = document.createElement("span"); tx.className = "txt";
li.appendChild(nm); li.appendChild(tx); ol.appendChild(li);
}
}
for (var j = 0; j < 4; j++) {
var li2 = ol.children[j];
li2.className = done[j] ? "done" : (j === cur ? "now" : "");
li2.children[0].textContent = done[j] ? "✓" : String(j + 1);
li2.children[1].textContent = STEPS[j];
}
}
function render() {
computeLayout();
svg.setAttribute("viewBox", "0 0 " + L.W + " " + L.H);
drawBoards(); drawGhosts(); drawStage(); labelPieces(); readouts(); kick();
}
/* ---------- pointer coords ---------- */
function toLocal(ev) {
var m = svg.getScreenCTM();
if (!m) return { x: 0, y: 0 };
m = m.inverse();
if (window.DOMPoint) return new DOMPoint(ev.clientX, ev.clientY).matrixTransform(m);
var pt = svg.createSVGPoint(); pt.x = ev.clientX; pt.y = ev.clientY;
return pt.matrixTransform(m);
}
function findSlot(p, x, y) {
var best = null;
for (var b = 0; b < 2; b++) for (var i = 0; i < 4; i++) {
if (ROTS[i] !== p.rot) continue;
if (pieceAt(b, i)) continue;
var bd = L.boards[b], sp = slotPos(b, i, sideA(), sideB());
var d = Math.hypot(x - (bd.x + sp.x * U), y - (bd.y + sp.y * U));
if (d < SNAP && (!best || d < best.d)) best = { b: b, i: i, d: d };
}
return best;
}
/* first free outline that faces the same way as this piece; right box first,
since the left one starts assembled */
function autoPlace(p) {
var order = boardFull(1) ? [0, 1] : [1, 0];
for (var o = 0; o < 2; o++) {
var b = order[o];
for (var i = 0; i < 4; i++) if (ROTS[i] === p.rot && !pieceAt(b, i)) return { b: b, i: i };
}
return null;
}
function tookIt(t) {
return "Placed in the " + BOARDNAME[t.b] + " box." + (proved() ? " Both boxes full — the holes match." : "");
}
function bindPiece(p) {
var g = p.el.g;
g.addEventListener("pointerdown", function (ev) {
if (ev.button !== undefined && ev.button !== 0 && ev.pointerType === "mouse") return;
if (st.drag) return; /* one piece at a time (second finger) */
ev.preventDefault();
var q = toLocal(ev);
var k = p.cur.k || 1;
var off = { x: (q.x - p.cur.x) / k, y: (q.y - p.cur.y) / k };
var from = p.slot;
p.slot = null;
st.drag = { p: p, off: off, moved: false, from: from, sx: q.x, sy: q.y };
p.cur.k = 1;
p.cur.x = q.x - off.x; p.cur.y = q.y - off.y;
g.classList.add("dragging");
gP.appendChild(g);
try { g.setPointerCapture(ev.pointerId); } catch (e) {}
drawGhosts(); drawStage(); drawBoards(); readouts(); kick();
});
g.addEventListener("pointermove", function (ev) {
if (!st.drag || st.drag.p !== p) return;
ev.preventDefault();
var q = toLocal(ev);
/* a couple of stray pixels between press and release is a click, not a drag */
if (Math.abs(q.x - st.drag.sx) + Math.abs(q.y - st.drag.sy) > 5) st.drag.moved = true;
p.cur.x = q.x - st.drag.off.x;
p.cur.y = q.y - st.drag.off.y;
var f = findSlot(p, p.cur.x, p.cur.y);
var changed = (!!f !== !!st.cand) || (f && st.cand && (f.b !== st.cand.b || f.i !== st.cand.i));
st.cand = f ? { b: f.b, i: f.i } : null;
if (changed) drawGhosts();
kick();
});
function end(ev) {
if (!st.drag || st.drag.p !== p) return;
try { g.releasePointerCapture(ev.pointerId); } catch (e) {}
g.classList.remove("dragging");
var moved = st.drag.moved, from = st.drag.from;
var f = moved ? findSlot(p, p.cur.x, p.cur.y) : null;
st.drag = null; st.cand = null;
if (!moved) {
/* tap / click, no drag: behave exactly like pressing Enter on the piece */
if (from) say("Taken out, back in the rack.");
else {
var t = autoPlace(p);
if (t) { p.slot = t; say(tookIt(t)); }
else say("No free outline facing this way.");
}
} else if (f) {
p.slot = { b: f.b, i: f.i };
say("Snapped into the " + BOARDNAME[f.b] + " box." + (proved() ? " Both boxes full — the holes match." : ""));
} else {
p.el.tri.setAttribute("class", "tri miss");
p.missT = Date.now() + 520;
say("No matching outline there — back to the rack.");
}
render();
}
g.addEventListener("pointerup", end);
g.addEventListener("pointercancel", end);
g.addEventListener("keydown", function (ev) {
if (ev.key !== "Enter" && ev.key !== " " && ev.key !== "Spacebar") return;
ev.preventDefault();
if (p.slot) { p.slot = null; say("Taken out, back in the rack."); }
else {
var placed = autoPlace(p);
if (placed) { p.slot = placed; say(tookIt(placed)); }
else say("No free outline facing this way.");
}
render();
});
}
/* ---------- controls ---------- */
$("shape").addEventListener("input", function () {
st.a = Math.round(parseFloat(this.value) * 10) / 10;
render();
});
$("p345").addEventListener("click", function () {
st.a = 3; $("shape").value = "3"; render(); say("Set to the 3, 4, 5 triangle.");
});
$("solve").addEventListener("click", function () {
for (var b = 0; b < 2; b++) for (var i = 0; i < 4; i++) {
if (pieceAt(b, i)) continue;
for (var n = 0; n < 8; n++) {
var p = st.pieces[n];
if (!p.slot && p.rot === ROTS[i]) { p.slot = { b: b, i: i }; break; }
}
}
render(); say("All eight placed. Both leftover holes are now shown.");
});
$("empty").addEventListener("click", function () {
for (var n = 0; n < 8; n++) st.pieces[n].slot = null;
render(); say("Both boxes emptied.");
});
$("reset").addEventListener("click", function () {
st.a = 3; $("shape").value = "3"; initialPlacement(); render(); say("Reset.");
});
$("guide").addEventListener("click", function () {
st.guide = !st.guide;
this.setAttribute("aria-pressed", st.guide ? "true" : "false");
render();
});
if (mqN.addEventListener) mqN.addEventListener("change", render);
else if (mqN.addListener) mqN.addListener(render);
/* first paint: pieces start at their targets so nothing flies in */
computeLayout();
for (var z = 0; z < 8; z++) { var t0 = targetOf(st.pieces[z]); st.pieces[z].cur = { x: t0.x, y: t0.y, k: t0.k }; }
render();
})();
</script>
</body>
</html>
source-visible by construction · nothing is published here without its code
Comments
0 totalNo comments yet. If you ran it, say what happened.