The demo below shows a triangulated triangle. Click any vertex to cycle its color through red, green, and blue. The three corner vertices are pre-colored and locked. The boundary vertices may only use the two colors of their edge — the demo enforces this automatically.
<!-- Sperner’s Lemma demo — color vertices, find rainbow triangles -->
<p class="hint">Click any unlocked vertex to cycle its color. Corner vertices are fixed. Edge vertices follow Sperner’s boundary rule (two colors only). A rainbow triangle (all three colors) is guaranteed to exist — try to avoid it!</p>
<div id="canvas-wrap">
<canvas id="tri-canvas" width="380" height="340"></canvas>
</div>
<div class="status" id="status">Click vertices to change colors.</div>
<div class="btns">
<button id="btn-find" type="button">Highlight rainbow triangles</button>
<button id="btn-reset" type="button" class="ghost">Reset colors</button>
</div>
/* Sperner demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
#canvas-wrap { display: flex; justify-content: center; }
canvas { cursor: pointer; border-radius: 8px; max-width: 100%; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.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; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Sperner’s Lemma interactive demo
// Rule: color vertices with 3 colors; corners get one each; boundary vertices use only their edge’s two colors.
const COLORS = ['red','green','blue'];
const COLOR_FILL = ['#e63946','#2a9d3a','#1d6fbf'];
const COLOR_STROKE = ['#a01020','#1a6b28','#114e8c'];
const RAINBOW_HIGHLIGHT = '#f4d03f'; // gold highlight for rainbow triangles
// Triangulation grid depth N — the big triangle is split into N^2 small triangles
// A depth-4 grid gives 16 upward + 12 downward = 28 small triangles and 15 vertices.
const N = 4; // depth of triangulation
// Build vertex list in row-major order: row 0 has 1 vertex, row 1 has 2, etc.
// Each vertex stores its grid position, pixel coords, color index (-1 = unset), and locked flag.
function buildVertices() {
const verts = [];
// Triangle apex at top-center; bottom-left = green; bottom-right = blue; top = red.
const cx = 190, cy = 320, side = 340;
const dx = side / N;
const dxhalf = dx / 2;
const dy = (side * Math.sqrt(3) / 2) / N;
for (let row = 0; row <= N; row++) {
for (let col = 0; col <= row; col++) {
const x = cx - row * dxhalf + col * dx;
const y = cy - row * dy;
verts.push({ row, col, x, y, color: -1, locked: false });
}
}
return verts;
}
// Flat index for (row, col) — row r has r+1 vertices starting at r*(r+1)/2.
function idx(row, col) { return row * (row + 1) / 2 + col; }
// Build all small triangles: each row produces upward and downward triangles.
function buildTriangles(verts) {
const tris = [];
for (let row = 0; row < N; row++) {
for (let col = 0; col <= row; col++) {
// upward-pointing triangle
tris.push([idx(row,col), idx(row+1,col), idx(row+1,col+1)]);
if (col < row) {
// downward-pointing triangle
tris.push([idx(row,col), idx(row,col+1), idx(row+1,col+1)]);
}
}
}
return tris;
}
const verts = buildVertices();
const tris = buildTriangles(verts);
// Initialize Sperner coloring: lock the three corners with distinct colors.
// Corner rule: top=red(0), bottom-left=green(1), bottom-right=blue(2)
verts[idx(0,0)].color = 0; verts[idx(0,0)].locked = true; // top corner: red
verts[idx(N,0)].color = 1; verts[idx(N,0)].locked = true; // bottom-left corner: green
verts[idx(N,N)].color = 2; verts[idx(N,N)].locked = true; // bottom-right corner: blue
// Allowed colors for each vertex based on Sperner’s boundary rule.
function getAllowedColors(v) {
const { row, col } = v;
const isTopLeft = (col === 0 && row > 0 && row < N); // left edge: between top(red) and bottom-left(green) => allowed red,green
const isTopRight = (col === row && row > 0 && row < N); // right edge: between top(red) and bottom-right(blue) => allowed red,blue
const isBottom = (row === N && col > 0 && col < N); // bottom edge: between bottom-left(green) and bottom-right(blue) => allowed green,blue
if (isTopLeft) return [0, 1]; // top-left edge: red or green
if (isTopRight) return [0, 2]; // top-right edge: red or blue
if (isBottom) return [1, 2]; // bottom edge: green or blue
return [0, 1, 2]; // interior vertex: any color
}
// Initialize non-locked vertices with first allowed color.
verts.forEach(v => {
if (!v.locked) {
const allowed = getAllowedColors(v);
v.color = allowed[0];
}
});
const canvas = document.getElementById('tri-canvas');
const ctx = canvas.getContext('2d');
let rainbowTris = [];
// Find all rainbow triangles (triangles with all 3 distinct colors on vertices).
function findRainbow() {
rainbowTris = tris.filter(t => {
const cs = new Set(t.map(i => verts[i].color));
return cs.size === 3;
});
return rainbowTris;
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
findRainbow();
// Draw each small triangle; fill rainbow ones in gold.
tris.forEach(t => {
const isRainbow = rainbowTris.includes(t);
const [a, b, c] = t.map(i => verts[i]);
ctx.beginPath();
ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.lineTo(c.x, c.y);
ctx.closePath();
ctx.fillStyle = isRainbow ? RAINBOW_HIGHLIGHT : '#f0f4f8';
ctx.fill();
ctx.strokeStyle = '#8a9ab0';
ctx.lineWidth = 1;
ctx.stroke();
});
// Draw each vertex as a colored circle; locked corners get a thicker border.
verts.forEach(v => {
ctx.beginPath();
ctx.arc(v.x, v.y, v.locked ? 9 : 8, 0, Math.PI * 2);
ctx.fillStyle = COLOR_FILL[v.color];
ctx.fill();
ctx.strokeStyle = COLOR_STROKE[v.color];
ctx.lineWidth = v.locked ? 2.5 : 1.5;
ctx.stroke();
});
updateStatus();
}
function updateStatus() {
const s = document.getElementById('status');
const count = rainbowTris.length;
if (count === 0) {
s.textContent = 'No rainbow triangle yet — keep trying!';
s.className = 'status info';
} else {
s.textContent = fmt(count === 1 ? '%count% rainbow triangle found (gold). Sperner’s lemma holds!' : '%count% rainbow triangles found (gold). Sperner’s lemma holds!', { count: count });
s.className = 'status ok';
}
}
// Click nearest vertex within 22px to cycle its allowed colors.
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
let nearest = null, best = Infinity;
verts.forEach(v => {
const d = Math.hypot(v.x - mx, v.y - my);
if (d < best) { best = d; nearest = v; }
});
if (nearest && !nearest.locked && best < 22) {
const allowed = getAllowedColors(nearest);
const cur = allowed.indexOf(nearest.color);
nearest.color = allowed[(cur + 1) % allowed.length];
draw();
}
});
// Highlight rainbow triangles button handler.
document.getElementById('btn-find').addEventListener('click', () => {
const rb = findRainbow();
if (rb.length === 0) {
const s = document.getElementById('status');
s.textContent = 'No rainbow triangle yet — keep trying!';
s.className = 'status info';
}
draw();
console.log('Rainbow triangles found:', rb.length);
});
// Reset all non-locked vertices to first allowed color.
document.getElementById('btn-reset').addEventListener('click', () => {
verts.forEach(v => {
if (!v.locked) {
const allowed = getAllowedColors(v);
v.color = allowed[0];
}
});
draw();
});
draw();
<!-- Sperner’s Lemma demo — color vertices, find rainbow triangles -->
<p class="hint">Click any unlocked vertex to cycle its color. Corner vertices are fixed. Edge vertices follow Sperner’s boundary rule (two colors only). A rainbow triangle (all three colors) is guaranteed to exist — try to avoid it!</p>
<div id="canvas-wrap">
<canvas id="tri-canvas" width="380" height="340"></canvas>
</div>
<div class="status" id="status">Click vertices to change colors.</div>
<div class="btns">
<button id="btn-find" type="button">Highlight rainbow triangles</button>
<button id="btn-reset" type="button" class="ghost">Reset colors</button>
</div>
/* Sperner demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
#canvas-wrap { display: flex; justify-content: center; }
canvas { cursor: pointer; border-radius: 8px; max-width: 100%; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.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; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Sperner’s Lemma interactive demo
// Rule: color vertices with 3 colors; corners get one each; boundary vertices use only their edge’s two colors.
const COLORS = ['red','green','blue'];
const COLOR_FILL = ['#e63946','#2a9d3a','#1d6fbf'];
const COLOR_STROKE = ['#a01020','#1a6b28','#114e8c'];
const RAINBOW_HIGHLIGHT = '#f4d03f'; // gold highlight for rainbow triangles
// Triangulation grid depth N — the big triangle is split into N^2 small triangles
// A depth-4 grid gives 16 upward + 12 downward = 28 small triangles and 15 vertices.
const N = 4; // depth of triangulation
// Build vertex list in row-major order: row 0 has 1 vertex, row 1 has 2, etc.
// Each vertex stores its grid position, pixel coords, color index (-1 = unset), and locked flag.
function buildVertices() {
const verts = [];
// Triangle apex at top-center; bottom-left = green; bottom-right = blue; top = red.
const cx = 190, cy = 320, side = 340;
const dx = side / N;
const dxhalf = dx / 2;
const dy = (side * Math.sqrt(3) / 2) / N;
for (let row = 0; row <= N; row++) {
for (let col = 0; col <= row; col++) {
const x = cx - row * dxhalf + col * dx;
const y = cy - row * dy;
verts.push({ row, col, x, y, color: -1, locked: false });
}
}
return verts;
}
// Flat index for (row, col) — row r has r+1 vertices starting at r*(r+1)/2.
function idx(row, col) { return row * (row + 1) / 2 + col; }
// Build all small triangles: each row produces upward and downward triangles.
function buildTriangles(verts) {
const tris = [];
for (let row = 0; row < N; row++) {
for (let col = 0; col <= row; col++) {
// upward-pointing triangle
tris.push([idx(row,col), idx(row+1,col), idx(row+1,col+1)]);
if (col < row) {
// downward-pointing triangle
tris.push([idx(row,col), idx(row,col+1), idx(row+1,col+1)]);
}
}
}
return tris;
}
const verts = buildVertices();
const tris = buildTriangles(verts);
// Initialize Sperner coloring: lock the three corners with distinct colors.
// Corner rule: top=red(0), bottom-left=green(1), bottom-right=blue(2)
verts[idx(0,0)].color = 0; verts[idx(0,0)].locked = true; // top corner: red
verts[idx(N,0)].color = 1; verts[idx(N,0)].locked = true; // bottom-left corner: green
verts[idx(N,N)].color = 2; verts[idx(N,N)].locked = true; // bottom-right corner: blue
// Allowed colors for each vertex based on Sperner’s boundary rule.
function getAllowedColors(v) {
const { row, col } = v;
const isTopLeft = (col === 0 && row > 0 && row < N); // left edge: between top(red) and bottom-left(green) => allowed red,green
const isTopRight = (col === row && row > 0 && row < N); // right edge: between top(red) and bottom-right(blue) => allowed red,blue
const isBottom = (row === N && col > 0 && col < N); // bottom edge: between bottom-left(green) and bottom-right(blue) => allowed green,blue
if (isTopLeft) return [0, 1]; // top-left edge: red or green
if (isTopRight) return [0, 2]; // top-right edge: red or blue
if (isBottom) return [1, 2]; // bottom edge: green or blue
return [0, 1, 2]; // interior vertex: any color
}
// Initialize non-locked vertices with first allowed color.
verts.forEach(v => {
if (!v.locked) {
const allowed = getAllowedColors(v);
v.color = allowed[0];
}
});
const canvas = document.getElementById('tri-canvas');
const ctx = canvas.getContext('2d');
let rainbowTris = [];
// Find all rainbow triangles (triangles with all 3 distinct colors on vertices).
function findRainbow() {
rainbowTris = tris.filter(t => {
const cs = new Set(t.map(i => verts[i].color));
return cs.size === 3;
});
return rainbowTris;
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
findRainbow();
// Draw each small triangle; fill rainbow ones in gold.
tris.forEach(t => {
const isRainbow = rainbowTris.includes(t);
const [a, b, c] = t.map(i => verts[i]);
ctx.beginPath();
ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.lineTo(c.x, c.y);
ctx.closePath();
ctx.fillStyle = isRainbow ? RAINBOW_HIGHLIGHT : '#f0f4f8';
ctx.fill();
ctx.strokeStyle = '#8a9ab0';
ctx.lineWidth = 1;
ctx.stroke();
});
// Draw each vertex as a colored circle; locked corners get a thicker border.
verts.forEach(v => {
ctx.beginPath();
ctx.arc(v.x, v.y, v.locked ? 9 : 8, 0, Math.PI * 2);
ctx.fillStyle = COLOR_FILL[v.color];
ctx.fill();
ctx.strokeStyle = COLOR_STROKE[v.color];
ctx.lineWidth = v.locked ? 2.5 : 1.5;
ctx.stroke();
});
updateStatus();
}
function updateStatus() {
const s = document.getElementById('status');
const count = rainbowTris.length;
if (count === 0) {
s.textContent = 'No rainbow triangle yet — keep trying!';
s.className = 'status info';
} else {
s.textContent = fmt(count === 1 ? '%count% rainbow triangle found (gold). Sperner’s lemma holds!' : '%count% rainbow triangles found (gold). Sperner’s lemma holds!', { count: count });
s.className = 'status ok';
}
}
// Click nearest vertex within 22px to cycle its allowed colors.
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
let nearest = null, best = Infinity;
verts.forEach(v => {
const d = Math.hypot(v.x - mx, v.y - my);
if (d < best) { best = d; nearest = v; }
});
if (nearest && !nearest.locked && best < 22) {
const allowed = getAllowedColors(nearest);
const cur = allowed.indexOf(nearest.color);
nearest.color = allowed[(cur + 1) % allowed.length];
draw();
}
});
// Highlight rainbow triangles button handler.
document.getElementById('btn-find').addEventListener('click', () => {
const rb = findRainbow();
if (rb.length === 0) {
const s = document.getElementById('status');
s.textContent = 'No rainbow triangle yet — keep trying!';
s.className = 'status info';
}
draw();
console.log('Rainbow triangles found:', rb.length);
});
// Reset all non-locked vertices to first allowed color.
document.getElementById('btn-reset').addEventListener('click', () => {
verts.forEach(v => {
if (!v.locked) {
const allowed = getAllowedColors(v);
v.color = allowed[0];
}
});
draw();
});
draw();
Every time you finish a valid Sperner coloring, the demo highlights the rainbow triangles in gold. Try to color the triangulation so that no rainbow triangle appears — you will find it is impossible. The lemma guarantees at least one always exists, and often there are several (always an odd number, in fact).
Comments
Loading comments...