<p class="hint">This polygon is <b>y-monotone</b>: a horizontal line crosses its boundary at most twice at any height. The sweep visits every vertex top to bottom, using a stack to decide exactly when a diagonal closes off a triangle.</p>
<svg id="stage" viewBox="0 0 320 320" class="stage"></svg>
<div class="panel">
<div class="stackbox">
<div class="stacklabel">Stack</div>
<div id="stackview" class="stackview"></div>
</div>
<div class="status" id="status">Press Step to start the sweep.</div>
</div>
<div class="btns">
<button id="step" type="button">Step</button>
<button id="auto" type="button">Auto-play</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.stage { width: 100%; max-width: 320px; height: 260px; display: block; margin: 0 auto; background: #f7f9fb;
border: 1px solid #dde3e9; border-radius: 8px; }
.poly-fill { fill: #e8eef3; stroke: none; }
.edge { fill: none; stroke: #1d3557; stroke-width: 2; }
.diagonal { fill: none; stroke: #e63946; stroke-width: 2; stroke-dasharray: 5 4; }
.vtx { fill: #1d3557; }
.vtx.done { fill: #adb1b8; }
.vtx.current { fill: #e63946; }
.panel { display: flex; align-items: flex-start; justify-content: space-between; gap: .75rem;
margin: .6rem 0; flex-wrap: wrap; }
.stackbox { background: #fff; border: 1px solid #dde3e9; border-radius: 8px; padding: .4rem .6rem; min-width: 110px; }
.stacklabel { font-size: .75rem; font-weight: 700; color: #1d3557; text-transform: uppercase; letter-spacing: .03em; margin-bottom: .3rem; }
.stackview { display: flex; flex-direction: column-reverse; gap: 3px; min-height: 1.4em; }
.stackitem { font: 700 13px ui-monospace, monospace; background: #e8eef3; color: #1d3557; border-radius: 5px;
padding: .1rem .4rem; text-align: center; }
.status { font-size: .95rem; font-weight: 600; flex: 1; min-width: 160px; }
.status.ok { color: #0a7d33; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .5; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// The 8 vertices of a y-monotone polygon, each tagged with which chain (Left or Right) it belongs to.
const V = [
{ x: 150, y: 20, chain: 'L' },
{ x: 90, y: 90, chain: 'L' },
{ x: 230, y: 110, chain: 'R' },
{ x: 60, y: 160, chain: 'L' },
{ x: 260, y: 180, chain: 'R' },
{ x: 120, y: 230, chain: 'L' },
{ x: 200, y: 250, chain: 'R' },
{ x: 150, y: 300, chain: 'L' },
];
// Vertex indices walked around the polygon boundary, used only for drawing the outline.
const BOUNDARY = [0, 1, 3, 5, 7, 6, 4, 2];
// Vertices sorted top to bottom by y — this is the order the sweep algorithm processes them in.
const ORDER = [0, 1, 2, 3, 4, 5, 6, 7];
function cross(o, a, b) { return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x); }
// Runs the full stack-sweep once up front so the step count and diagonal total can be checked/reported.
function computeDiagonals() {
const diagonals = [];
const stack = [ORDER[0], ORDER[1]];
for (let j = 2; j < ORDER.length - 1; j++) {
const vi = ORDER[j];
const top = stack[stack.length - 1];
if (V[vi].chain !== V[top].chain) {
while (stack.length > 1) diagonals.push([vi, stack.pop()]);
stack.pop();
stack.push(top);
stack.push(vi);
} else {
let last = stack.pop();
while (stack.length > 0) {
const prev = stack[stack.length - 1];
const turn = cross(V[prev], V[last], V[vi]);
const isValid = V[vi].chain === 'L' ? turn > 0 : turn < 0;
if (isValid) { diagonals.push([vi, prev]); last = stack.pop(); } else break;
}
stack.push(last);
stack.push(vi);
}
}
const last = ORDER[ORDER.length - 1];
for (let k = 1; k < stack.length - 1; k++) diagonals.push([last, stack[k]]);
return diagonals;
}
const ALL_DIAGONALS = computeDiagonals(); // For n vertices a y-monotone polygon triangulates into exactly n-3 diagonals and n-2 triangles.
const NS = 'http://www.w3.org/2000/svg';
const svg = document.getElementById('stage');
let stepIdx = 0; // Index into ORDER of the next vertex the sweep will process.
let stack = [];
let drawnDiagonals = [];
let autoTimer = null;
function poly(el, cls) { el.setAttribute('class', cls); return el; }
function render() {
svg.innerHTML = '';
const fillPts = BOUNDARY.map(i => V[i].x + ',' + V[i].y).join(' ');
const fillPoly = document.createElementNS(NS, 'polygon');
fillPoly.setAttribute('points', fillPts);
poly(fillPoly, 'poly-fill');
svg.appendChild(fillPoly);
const edgePath = document.createElementNS(NS, 'polygon');
edgePath.setAttribute('points', fillPts);
poly(edgePath, 'edge');
svg.appendChild(edgePath);
for (const [a, b] of drawnDiagonals) {
const line = document.createElementNS(NS, 'line');
line.setAttribute('x1', V[a].x); line.setAttribute('y1', V[a].y);
line.setAttribute('x2', V[b].x); line.setAttribute('y2', V[b].y);
poly(line, 'diagonal');
svg.appendChild(line);
}
for (let i = 0; i < V.length; i++) {
const circ = document.createElementNS(NS, 'circle');
circ.setAttribute('cx', V[i].x); circ.setAttribute('cy', V[i].y); circ.setAttribute('r', 5);
const seenPos = ORDER.indexOf(i);
let cls = 'vtx';
if (seenPos < stepIdx) cls += ' done';
if (seenPos === stepIdx - 1) cls += ' current';
poly(circ, cls);
svg.appendChild(circ);
}
const stackview = document.getElementById('stackview');
stackview.innerHTML = '';
for (const idx of stack) {
const d = document.createElement('div');
d.className = 'stackitem'; d.textContent = 'v' + idx;
stackview.appendChild(d);
}
}
function setStatus(t, cls) {
const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + (cls || '');
}
// One step of the classic stack-sweep triangulation (Garey, Johnson, Preparata, Tarjan 1978).
function doStep() {
if (stepIdx >= ORDER.length) return false;
if (stepIdx === 0) { stack = [ORDER[0]]; stepIdx = 1; setStatus(fmt('Pushed v%v% onto the stack (same chain ahead, nothing to cut yet).', { v: ORDER[0] })); render(); return true; }
if (stepIdx === 1) { stack.push(ORDER[1]); stepIdx = 2; setStatus(fmt('Pushed v%v% onto the stack (same chain ahead, nothing to cut yet).', { v: ORDER[1] })); render(); return true; }
const vi = ORDER[stepIdx];
const top = stack[stack.length - 1];
let addedCount = 0;
if (stepIdx === ORDER.length - 1) {
for (let k = 1; k < stack.length - 1; k++) { drawnDiagonals.push([vi, stack[k]]); addedCount++; }
stack = [stack[0], vi];
setStatus(fmt('Final vertex: connected to the %n% remaining stack vertex/vertices.', { n: addedCount }), 'ok');
} else if (V[vi].chain !== V[top].chain) {
while (stack.length > 1) { drawnDiagonals.push([vi, stack.pop()]); addedCount++; }
stack.pop();
stack.push(top);
stack.push(vi);
setStatus(fmt('v%v% is on the opposite chain: cut %n% diagonal(s) to every vertex left on the stack.', { v: vi, n: addedCount }));
} else {
let last = stack.pop();
while (stack.length > 0) {
const prev = stack[stack.length - 1];
const turn = cross(V[prev], V[last], V[vi]);
const isValid = V[vi].chain === 'L' ? turn > 0 : turn < 0;
if (isValid) { drawnDiagonals.push([vi, prev]); last = stack.pop(); addedCount++; } else break;
}
stack.push(last);
stack.push(vi);
setStatus(addedCount > 0 ? fmt('v%v% is on the same chain: popped and cut %n% diagonal(s) while the turn stayed convex.', { v: vi, n: addedCount }) : fmt('v%v% is on the same chain, but no valid diagonal yet — just pushed.', { v: vi }));
}
stepIdx++;
render();
if (stepIdx >= ORDER.length) {
setStatus(fmt('Done — %n% diagonals drawn, splitting the polygon into n-2 triangles.', { n: drawnDiagonals.length }), 'ok');
document.getElementById('step').disabled = true;
stopAuto();
}
return true;
}
function stopAuto() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; document.getElementById('auto').textContent = 'Auto-play'; } }
document.getElementById('step').addEventListener('click', doStep);
document.getElementById('auto').addEventListener('click', () => {
if (autoTimer) { stopAuto(); return; }
document.getElementById('auto').textContent = 'Stop';
autoTimer = setInterval(() => { if (!doStep()) stopAuto(); }, 700);
});
document.getElementById('reset').addEventListener('click', () => {
stopAuto();
stepIdx = 0; stack = []; drawnDiagonals = [];
document.getElementById('step').disabled = false;
setStatus('Press Step to start the sweep.');
render();
});
render();
<p class="hint">This polygon is <b>y-monotone</b>: a horizontal line crosses its boundary at most twice at any height. The sweep visits every vertex top to bottom, using a stack to decide exactly when a diagonal closes off a triangle.</p>
<svg id="stage" viewBox="0 0 320 320" class="stage"></svg>
<div class="panel">
<div class="stackbox">
<div class="stacklabel">Stack</div>
<div id="stackview" class="stackview"></div>
</div>
<div class="status" id="status">Press Step to start the sweep.</div>
</div>
<div class="btns">
<button id="step" type="button">Step</button>
<button id="auto" type="button">Auto-play</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.stage { width: 100%; max-width: 320px; height: 260px; display: block; margin: 0 auto; background: #f7f9fb;
border: 1px solid #dde3e9; border-radius: 8px; }
.poly-fill { fill: #e8eef3; stroke: none; }
.edge { fill: none; stroke: #1d3557; stroke-width: 2; }
.diagonal { fill: none; stroke: #e63946; stroke-width: 2; stroke-dasharray: 5 4; }
.vtx { fill: #1d3557; }
.vtx.done { fill: #adb1b8; }
.vtx.current { fill: #e63946; }
.panel { display: flex; align-items: flex-start; justify-content: space-between; gap: .75rem;
margin: .6rem 0; flex-wrap: wrap; }
.stackbox { background: #fff; border: 1px solid #dde3e9; border-radius: 8px; padding: .4rem .6rem; min-width: 110px; }
.stacklabel { font-size: .75rem; font-weight: 700; color: #1d3557; text-transform: uppercase; letter-spacing: .03em; margin-bottom: .3rem; }
.stackview { display: flex; flex-direction: column-reverse; gap: 3px; min-height: 1.4em; }
.stackitem { font: 700 13px ui-monospace, monospace; background: #e8eef3; color: #1d3557; border-radius: 5px;
padding: .1rem .4rem; text-align: center; }
.status { font-size: .95rem; font-weight: 600; flex: 1; min-width: 160px; }
.status.ok { color: #0a7d33; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .5; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// The 8 vertices of a y-monotone polygon, each tagged with which chain (Left or Right) it belongs to.
const V = [
{ x: 150, y: 20, chain: 'L' },
{ x: 90, y: 90, chain: 'L' },
{ x: 230, y: 110, chain: 'R' },
{ x: 60, y: 160, chain: 'L' },
{ x: 260, y: 180, chain: 'R' },
{ x: 120, y: 230, chain: 'L' },
{ x: 200, y: 250, chain: 'R' },
{ x: 150, y: 300, chain: 'L' },
];
// Vertex indices walked around the polygon boundary, used only for drawing the outline.
const BOUNDARY = [0, 1, 3, 5, 7, 6, 4, 2];
// Vertices sorted top to bottom by y — this is the order the sweep algorithm processes them in.
const ORDER = [0, 1, 2, 3, 4, 5, 6, 7];
function cross(o, a, b) { return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x); }
// Runs the full stack-sweep once up front so the step count and diagonal total can be checked/reported.
function computeDiagonals() {
const diagonals = [];
const stack = [ORDER[0], ORDER[1]];
for (let j = 2; j < ORDER.length - 1; j++) {
const vi = ORDER[j];
const top = stack[stack.length - 1];
if (V[vi].chain !== V[top].chain) {
while (stack.length > 1) diagonals.push([vi, stack.pop()]);
stack.pop();
stack.push(top);
stack.push(vi);
} else {
let last = stack.pop();
while (stack.length > 0) {
const prev = stack[stack.length - 1];
const turn = cross(V[prev], V[last], V[vi]);
const isValid = V[vi].chain === 'L' ? turn > 0 : turn < 0;
if (isValid) { diagonals.push([vi, prev]); last = stack.pop(); } else break;
}
stack.push(last);
stack.push(vi);
}
}
const last = ORDER[ORDER.length - 1];
for (let k = 1; k < stack.length - 1; k++) diagonals.push([last, stack[k]]);
return diagonals;
}
const ALL_DIAGONALS = computeDiagonals(); // For n vertices a y-monotone polygon triangulates into exactly n-3 diagonals and n-2 triangles.
const NS = 'http://www.w3.org/2000/svg';
const svg = document.getElementById('stage');
let stepIdx = 0; // Index into ORDER of the next vertex the sweep will process.
let stack = [];
let drawnDiagonals = [];
let autoTimer = null;
function poly(el, cls) { el.setAttribute('class', cls); return el; }
function render() {
svg.innerHTML = '';
const fillPts = BOUNDARY.map(i => V[i].x + ',' + V[i].y).join(' ');
const fillPoly = document.createElementNS(NS, 'polygon');
fillPoly.setAttribute('points', fillPts);
poly(fillPoly, 'poly-fill');
svg.appendChild(fillPoly);
const edgePath = document.createElementNS(NS, 'polygon');
edgePath.setAttribute('points', fillPts);
poly(edgePath, 'edge');
svg.appendChild(edgePath);
for (const [a, b] of drawnDiagonals) {
const line = document.createElementNS(NS, 'line');
line.setAttribute('x1', V[a].x); line.setAttribute('y1', V[a].y);
line.setAttribute('x2', V[b].x); line.setAttribute('y2', V[b].y);
poly(line, 'diagonal');
svg.appendChild(line);
}
for (let i = 0; i < V.length; i++) {
const circ = document.createElementNS(NS, 'circle');
circ.setAttribute('cx', V[i].x); circ.setAttribute('cy', V[i].y); circ.setAttribute('r', 5);
const seenPos = ORDER.indexOf(i);
let cls = 'vtx';
if (seenPos < stepIdx) cls += ' done';
if (seenPos === stepIdx - 1) cls += ' current';
poly(circ, cls);
svg.appendChild(circ);
}
const stackview = document.getElementById('stackview');
stackview.innerHTML = '';
for (const idx of stack) {
const d = document.createElement('div');
d.className = 'stackitem'; d.textContent = 'v' + idx;
stackview.appendChild(d);
}
}
function setStatus(t, cls) {
const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + (cls || '');
}
// One step of the classic stack-sweep triangulation (Garey, Johnson, Preparata, Tarjan 1978).
function doStep() {
if (stepIdx >= ORDER.length) return false;
if (stepIdx === 0) { stack = [ORDER[0]]; stepIdx = 1; setStatus(fmt('Pushed v%v% onto the stack (same chain ahead, nothing to cut yet).', { v: ORDER[0] })); render(); return true; }
if (stepIdx === 1) { stack.push(ORDER[1]); stepIdx = 2; setStatus(fmt('Pushed v%v% onto the stack (same chain ahead, nothing to cut yet).', { v: ORDER[1] })); render(); return true; }
const vi = ORDER[stepIdx];
const top = stack[stack.length - 1];
let addedCount = 0;
if (stepIdx === ORDER.length - 1) {
for (let k = 1; k < stack.length - 1; k++) { drawnDiagonals.push([vi, stack[k]]); addedCount++; }
stack = [stack[0], vi];
setStatus(fmt('Final vertex: connected to the %n% remaining stack vertex/vertices.', { n: addedCount }), 'ok');
} else if (V[vi].chain !== V[top].chain) {
while (stack.length > 1) { drawnDiagonals.push([vi, stack.pop()]); addedCount++; }
stack.pop();
stack.push(top);
stack.push(vi);
setStatus(fmt('v%v% is on the opposite chain: cut %n% diagonal(s) to every vertex left on the stack.', { v: vi, n: addedCount }));
} else {
let last = stack.pop();
while (stack.length > 0) {
const prev = stack[stack.length - 1];
const turn = cross(V[prev], V[last], V[vi]);
const isValid = V[vi].chain === 'L' ? turn > 0 : turn < 0;
if (isValid) { drawnDiagonals.push([vi, prev]); last = stack.pop(); addedCount++; } else break;
}
stack.push(last);
stack.push(vi);
setStatus(addedCount > 0 ? fmt('v%v% is on the same chain: popped and cut %n% diagonal(s) while the turn stayed convex.', { v: vi, n: addedCount }) : fmt('v%v% is on the same chain, but no valid diagonal yet — just pushed.', { v: vi }));
}
stepIdx++;
render();
if (stepIdx >= ORDER.length) {
setStatus(fmt('Done — %n% diagonals drawn, splitting the polygon into n-2 triangles.', { n: drawnDiagonals.length }), 'ok');
document.getElementById('step').disabled = true;
stopAuto();
}
return true;
}
function stopAuto() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; document.getElementById('auto').textContent = 'Auto-play'; } }
document.getElementById('step').addEventListener('click', doStep);
document.getElementById('auto').addEventListener('click', () => {
if (autoTimer) { stopAuto(); return; }
document.getElementById('auto').textContent = 'Stop';
autoTimer = setInterval(() => { if (!doStep()) stopAuto(); }, 700);
});
document.getElementById('reset').addEventListener('click', () => {
stopAuto();
stepIdx = 0; stack = []; drawnDiagonals = [];
document.getElementById('step').disabled = false;
setStatus('Press Step to start the sweep.');
render();
});
render();
Each step does one of two cheap things: push a vertex onto the stack, or pop vertices off it while drawing diagonals, testing only whether a turn is convex. There is no searching for "the next valid cut" — the stack always tells you exactly which diagonal is safe to draw next. That is why the whole pass costs O(n): every vertex is pushed once and popped once.
Comments
Loading comments...