Start with a big bounding square — the "universe" before any constraint is applied. Every time you add a half-plane, the polygon gets clipped against it.
<p class="hint">The teal shape starts as a big square — the whole plane, boxed in for drawing. Each <b>constraint</b> is a straight line with an arrow-free side that must be kept; every click slices the shape down to what still satisfies <b>all</b> constraints so far.</p>
<svg id="canvas" class="canvas" viewBox="0 0 300 300"></svg>
<div class="status" id="status">No constraints yet — the full square is feasible.</div>
<div class="btns">
<button id="add" type="button">Add constraint</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 .7rem; line-height: 1.45; }
.canvas { width: 100%; max-width: 300px; height: 260px; background: #f4f6f8; border: 1px solid #cdd9e3;
border-radius: 8px; display: block; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; 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; }
button:disabled { opacity: .5; cursor: default; }
// Half-plane intersection demo: clip a convex polygon against a sequence of
const SIZE = 300;
const MAX_CONSTRAINTS = 7;
// random half-planes, exactly as an incremental (non-angle-sorted) implementation would.
let polygon = [[20, 20], [280, 20], [280, 280], [20, 280]];
let constraints = []; // Starting polygon is a bounding square; each add() clips it against one more half-plane.
// Sutherland-Hodgman clip of a convex polygon against a single half-plane.
// half-plane: points p with (p - point) . normal <= 0 survive
function clipPolygon(poly, point, normal) {
const inside = (p) => (p[0] - point[0]) * normal[0] + (p[1] - point[1]) * normal[1] <= 1e-9;
const intersect = (a, b) => {
const da = (a[0] - point[0]) * normal[0] + (a[1] - point[1]) * normal[1];
const db = (b[0] - point[0]) * normal[0] + (b[1] - point[1]) * normal[1];
const t = da / (da - db);
return [a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1])];
};
const out = [];
for (let i = 0; i < poly.length; i++) {
const cur = poly[i], nxt = poly[(i + 1) % poly.length];
const curIn = inside(cur), nxtIn = inside(nxt);
if (curIn) out.push(cur);
if (curIn !== nxtIn) out.push(intersect(cur, nxt));
}
return out;
}
// Pick a random line direction and offset from the center, so each constraint visibly cuts a chunk off.
function randomHalfPlane() {
const angle = Math.random() * Math.PI * 2;
const normal = [Math.cos(angle), Math.sin(angle)];
const cx = SIZE / 2, cy = SIZE / 2;
const r = 60 + Math.random() * 90;
const point = [cx + normal[0] * r, cy + normal[1] * r];
return { point, normal };
}
function polygonArea(poly) {
let a = 0;
for (let i = 0; i < poly.length; i++) {
const [x1, y1] = poly[i], [x2, y2] = poly[(i + 1) % poly.length];
a += x1 * y2 - x2 * y1;
}
return Math.abs(a) / 2;
}
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
function render() {
const svg = document.getElementById('canvas');
svg.innerHTML = '';
const ns = 'http://www.w3.org/2000/svg';
// Draw each constraint's boundary line as a long dashed segment across the canvas.
for (const cst of constraints) {
const { point, normal } = cst;
const dir = [-normal[1], normal[0]];
const p1 = [point[0] - dir[0] * 400, point[1] - dir[1] * 400];
const p2 = [point[0] + dir[0] * 400, point[1] + dir[1] * 400];
const line = document.createElementNS(ns, 'line');
line.setAttribute('x1', p1[0]); line.setAttribute('y1', p1[1]);
line.setAttribute('x2', p2[0]); line.setAttribute('y2', p2[1]);
line.setAttribute('stroke', '#c9ccd1');
line.setAttribute('stroke-width', '1.5');
line.setAttribute('stroke-dasharray', '4 3');
svg.appendChild(line);
}
if (polygon.length > 0) {
const pts = polygon.map((p) => p.join(',')).join(' ');
const poly = document.createElementNS(ns, 'polygon');
poly.setAttribute('points', pts);
poly.setAttribute('fill', '#a8dadc');
poly.setAttribute('fill-opacity', '0.55');
poly.setAttribute('stroke', '#1d3557');
poly.setAttribute('stroke-width', '2');
svg.appendChild(poly);
}
}
function updateStatus() {
const s = document.getElementById('status');
if (constraints.length === 0) {
s.textContent = 'No constraints yet — the full square is feasible.';
return;
}
if (polygon.length === 0) {
s.textContent = 'The constraints are contradictory — nothing satisfies them all.';
return;
}
s.textContent = fmt('%n% constraint(s) applied — the feasible region has %edges% edges and area %area%.', {
n: constraints.length,
edges: polygon.length,
area: Math.round(polygonArea(polygon)),
});
}
function addConstraint() {
if (constraints.length >= MAX_CONSTRAINTS) return;
const hp = randomHalfPlane();
constraints.push(hp);
polygon = clipPolygon(polygon, hp.point, hp.normal);
render();
updateStatus();
if (constraints.length >= MAX_CONSTRAINTS) document.getElementById('add').disabled = true;
console.log('Constraints so far:', constraints.length, '| polygon edges:', polygon.length);
}
function reset() {
polygon = [[20, 20], [280, 20], [280, 280], [20, 280]];
constraints = [];
document.getElementById('add').disabled = false;
render();
updateStatus();
}
document.getElementById('add').addEventListener('click', addConstraint);
document.getElementById('reset').addEventListener('click', reset);
render();
updateStatus();
<p class="hint">The teal shape starts as a big square — the whole plane, boxed in for drawing. Each <b>constraint</b> is a straight line with an arrow-free side that must be kept; every click slices the shape down to what still satisfies <b>all</b> constraints so far.</p>
<svg id="canvas" class="canvas" viewBox="0 0 300 300"></svg>
<div class="status" id="status">No constraints yet — the full square is feasible.</div>
<div class="btns">
<button id="add" type="button">Add constraint</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 .7rem; line-height: 1.45; }
.canvas { width: 100%; max-width: 300px; height: 260px; background: #f4f6f8; border: 1px solid #cdd9e3;
border-radius: 8px; display: block; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; 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; }
button:disabled { opacity: .5; cursor: default; }
// Half-plane intersection demo: clip a convex polygon against a sequence of
const SIZE = 300;
const MAX_CONSTRAINTS = 7;
// random half-planes, exactly as an incremental (non-angle-sorted) implementation would.
let polygon = [[20, 20], [280, 20], [280, 280], [20, 280]];
let constraints = []; // Starting polygon is a bounding square; each add() clips it against one more half-plane.
// Sutherland-Hodgman clip of a convex polygon against a single half-plane.
// half-plane: points p with (p - point) . normal <= 0 survive
function clipPolygon(poly, point, normal) {
const inside = (p) => (p[0] - point[0]) * normal[0] + (p[1] - point[1]) * normal[1] <= 1e-9;
const intersect = (a, b) => {
const da = (a[0] - point[0]) * normal[0] + (a[1] - point[1]) * normal[1];
const db = (b[0] - point[0]) * normal[0] + (b[1] - point[1]) * normal[1];
const t = da / (da - db);
return [a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1])];
};
const out = [];
for (let i = 0; i < poly.length; i++) {
const cur = poly[i], nxt = poly[(i + 1) % poly.length];
const curIn = inside(cur), nxtIn = inside(nxt);
if (curIn) out.push(cur);
if (curIn !== nxtIn) out.push(intersect(cur, nxt));
}
return out;
}
// Pick a random line direction and offset from the center, so each constraint visibly cuts a chunk off.
function randomHalfPlane() {
const angle = Math.random() * Math.PI * 2;
const normal = [Math.cos(angle), Math.sin(angle)];
const cx = SIZE / 2, cy = SIZE / 2;
const r = 60 + Math.random() * 90;
const point = [cx + normal[0] * r, cy + normal[1] * r];
return { point, normal };
}
function polygonArea(poly) {
let a = 0;
for (let i = 0; i < poly.length; i++) {
const [x1, y1] = poly[i], [x2, y2] = poly[(i + 1) % poly.length];
a += x1 * y2 - x2 * y1;
}
return Math.abs(a) / 2;
}
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
function render() {
const svg = document.getElementById('canvas');
svg.innerHTML = '';
const ns = 'http://www.w3.org/2000/svg';
// Draw each constraint's boundary line as a long dashed segment across the canvas.
for (const cst of constraints) {
const { point, normal } = cst;
const dir = [-normal[1], normal[0]];
const p1 = [point[0] - dir[0] * 400, point[1] - dir[1] * 400];
const p2 = [point[0] + dir[0] * 400, point[1] + dir[1] * 400];
const line = document.createElementNS(ns, 'line');
line.setAttribute('x1', p1[0]); line.setAttribute('y1', p1[1]);
line.setAttribute('x2', p2[0]); line.setAttribute('y2', p2[1]);
line.setAttribute('stroke', '#c9ccd1');
line.setAttribute('stroke-width', '1.5');
line.setAttribute('stroke-dasharray', '4 3');
svg.appendChild(line);
}
if (polygon.length > 0) {
const pts = polygon.map((p) => p.join(',')).join(' ');
const poly = document.createElementNS(ns, 'polygon');
poly.setAttribute('points', pts);
poly.setAttribute('fill', '#a8dadc');
poly.setAttribute('fill-opacity', '0.55');
poly.setAttribute('stroke', '#1d3557');
poly.setAttribute('stroke-width', '2');
svg.appendChild(poly);
}
}
function updateStatus() {
const s = document.getElementById('status');
if (constraints.length === 0) {
s.textContent = 'No constraints yet — the full square is feasible.';
return;
}
if (polygon.length === 0) {
s.textContent = 'The constraints are contradictory — nothing satisfies them all.';
return;
}
s.textContent = fmt('%n% constraint(s) applied — the feasible region has %edges% edges and area %area%.', {
n: constraints.length,
edges: polygon.length,
area: Math.round(polygonArea(polygon)),
});
}
function addConstraint() {
if (constraints.length >= MAX_CONSTRAINTS) return;
const hp = randomHalfPlane();
constraints.push(hp);
polygon = clipPolygon(polygon, hp.point, hp.normal);
render();
updateStatus();
if (constraints.length >= MAX_CONSTRAINTS) document.getElementById('add').disabled = true;
console.log('Constraints so far:', constraints.length, '| polygon edges:', polygon.length);
}
function reset() {
polygon = [[20, 20], [280, 20], [280, 280], [20, 280]];
constraints = [];
document.getElementById('add').disabled = false;
render();
updateStatus();
}
document.getElementById('add').addEventListener('click', addConstraint);
document.getElementById('reset').addEventListener('click', reset);
render();
updateStatus();
Comments
Loading comments...