Click anywhere on the canvas to drop endpoints and add a new segment. The lower envelope — the minimum-height visible curve — is redrawn in real time. Watch the segment count and the envelope-piece count side by side.
<!-- toolbar + canvas layout -->
<div class="toolbar">
<span class="info">Segments: <b id="seg-count">0</b> | Envelope pieces: <b id="env-count">0</b></span>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<p class="hint">Click once to place the first endpoint, click again to complete the segment. Add more segments to see the envelope grow.</p>
<canvas id="cv" width="560" height="320"></canvas>
<div class="status" id="status">Click anywhere to place the first endpoint of a segment.</div>
/* canvas demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: .4rem; }
.info { font-size: .9rem; color: #444; }
.hint { font-size: .85rem; color: #555; margin: 0 0 .4rem; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; cursor: crosshair;
background: #f8fafc; display: block; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-top: .45rem; color: #1d3557; }
button { font: 600 13px system-ui; padding: .4rem .85rem; 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] ?? ''));
// Lower-envelope demo: click to place segment endpoints two at a time.
// The bold colored overlay is the lower envelope (minimum-y curve).
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
// list of {x1,y1,x2,y2} ordered left-to-right
const segments = [];
// first endpoint waiting for its pair
let pending = null;
const COLORS = ['#e63946','#457b9d','#2a9d8f','#e9c46a','#6a4c93','#f4a261','#264653','#a8dadc'];
// evaluate segment y at a given x (assumes x in [x1,x2])
function segY(seg, x) {
const t = (x - seg.x1) / (seg.x2 - seg.x1);
return seg.y1 + t * (seg.y2 - seg.y1);
}
// compute the lower-envelope pieces as {x0,x1,idx} intervals
function computeEnvelope() {
const W = cv.width;
// critical x values: endpoints + intersections
const xs = new Set();
xs.add(0); xs.add(W);
segments.forEach(s => { xs.add(s.x1); xs.add(s.x2); });
// add all pairwise intersection x-coordinates
for (let i = 0; i < segments.length; i++) {
for (let j = i + 1; j < segments.length; j++) {
const xi = intersectX(segments[i], segments[j]);
if (xi !== null && xi > 0 && xi < W) xs.add(xi);
}
}
const sortedX = [...xs].sort((a, b) => a - b);
// sweep intervals, track which segment is minimum
const pieces = [];
let prevMin = null;
for (let k = 0; k < sortedX.length - 1; k++) {
const xMid = (sortedX[k] + sortedX[k + 1]) / 2;
let minY = Infinity, minIdx = -1;
segments.forEach((s, i) => {
if (xMid >= s.x1 && xMid <= s.x2) {
const y = segY(s, xMid);
if (y < minY) { minY = y; minIdx = i; }
}
});
if (minIdx !== -1) {
if (prevMin !== minIdx) {
pieces.push({ x0: sortedX[k], x1: sortedX[k + 1], idx: minIdx });
prevMin = minIdx;
} else {
pieces[pieces.length - 1].x1 = sortedX[k + 1];
}
} else {
prevMin = null;
}
}
return pieces;
}
// x-coordinate of intersection of two non-vertical lines, or null
function intersectX(a, b) {
const dya = a.y2 - a.y1, dxa = a.x2 - a.x1;
const dyb = b.y2 - b.y1, dxb = b.x2 - b.x1;
if (dxa === 0 || dxb === 0) return null;
const ma = dya / dxa, mb = dyb / dxb;
if (Math.abs(ma - mb) < 1e-10) return null;
const ba = a.y1 - ma * a.x1, bb = b.y1 - mb * b.x1;
return (bb - ba) / (ma - mb);
}
function draw() {
const W = cv.width, H = cv.height;
ctx.clearRect(0, 0, W, H);
// draw all segments faintly
segments.forEach((s, i) => {
ctx.strokeStyle = COLORS[i % COLORS.length];
ctx.lineWidth = 1.5;
ctx.globalAlpha = 0.55;
ctx.beginPath(); ctx.moveTo(s.x1, s.y1); ctx.lineTo(s.x2, s.y2); ctx.stroke();
ctx.globalAlpha = 1;
[{ x: s.x1, y: s.y1 }, { x: s.x2, y: s.y2 }].forEach(p => {
ctx.fillStyle = COLORS[i % COLORS.length];
ctx.beginPath(); ctx.arc(p.x, p.y, 3.5, 0, 2 * Math.PI); ctx.fill();
});
});
// draw the pending first endpoint
if (pending) {
ctx.strokeStyle = '#aaa';
ctx.setLineDash([4, 3]);
ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(pending.x, pending.y, 4, 0, 2 * Math.PI); ctx.stroke();
ctx.setLineDash([]);
}
// draw the lower envelope thickly
if (segments.length > 0) {
const pieces = computeEnvelope();
ctx.lineWidth = 3;
ctx.globalAlpha = 0.95;
pieces.forEach(p => {
const s = segments[p.idx];
ctx.strokeStyle = COLORS[p.idx % COLORS.length];
const y0 = segY(s, p.x0), y1 = segY(s, p.x1);
ctx.beginPath(); ctx.moveTo(p.x0, y0); ctx.lineTo(p.x1, y1); ctx.stroke();
});
ctx.globalAlpha = 1;
document.getElementById('env-count').textContent = pieces.length;
} else {
document.getElementById('env-count').textContent = 0;
}
document.getElementById('seg-count').textContent = segments.length;
}
function updateStatus() {
const n = segments.length;
if (n === 0) {
document.getElementById('status').textContent = 'Click anywhere to place the first endpoint of a segment.';
} else if (pending) {
document.getElementById('status').textContent = 'Now click a second point to complete the segment.';
} else {
const pieces = computeEnvelope().length;
const worst = n * n;
document.getElementById('status').textContent =
fmt('Envelope has %pieces% pieces for %n% segments — naïve worst case would be n² = %worst%.', { pieces: pieces, n: n, worst: worst });
}
}
cv.addEventListener('click', e => {
const rect = cv.getBoundingClientRect();
const scaleX = cv.width / rect.width;
const scaleY = cv.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
if (!pending) {
pending = { x, y };
} else {
if (Math.abs(x - pending.x) > 5) {
// normalize so x1 <= x2
const seg = pending.x < x
? { x1: pending.x, y1: pending.y, x2: x, y2: y }
: { x1: x, y1: y, x2: pending.x, y2: pending.y };
segments.push(seg);
}
pending = null;
}
draw();
updateStatus();
});
document.getElementById('btn-reset').addEventListener('click', () => {
segments.length = 0;
pending = null;
draw();
updateStatus();
});
draw();
updateStatus();
<!-- toolbar + canvas layout -->
<div class="toolbar">
<span class="info">Segments: <b id="seg-count">0</b> | Envelope pieces: <b id="env-count">0</b></span>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<p class="hint">Click once to place the first endpoint, click again to complete the segment. Add more segments to see the envelope grow.</p>
<canvas id="cv" width="560" height="320"></canvas>
<div class="status" id="status">Click anywhere to place the first endpoint of a segment.</div>
/* canvas demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: .4rem; }
.info { font-size: .9rem; color: #444; }
.hint { font-size: .85rem; color: #555; margin: 0 0 .4rem; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; cursor: crosshair;
background: #f8fafc; display: block; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-top: .45rem; color: #1d3557; }
button { font: 600 13px system-ui; padding: .4rem .85rem; 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] ?? ''));
// Lower-envelope demo: click to place segment endpoints two at a time.
// The bold colored overlay is the lower envelope (minimum-y curve).
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
// list of {x1,y1,x2,y2} ordered left-to-right
const segments = [];
// first endpoint waiting for its pair
let pending = null;
const COLORS = ['#e63946','#457b9d','#2a9d8f','#e9c46a','#6a4c93','#f4a261','#264653','#a8dadc'];
// evaluate segment y at a given x (assumes x in [x1,x2])
function segY(seg, x) {
const t = (x - seg.x1) / (seg.x2 - seg.x1);
return seg.y1 + t * (seg.y2 - seg.y1);
}
// compute the lower-envelope pieces as {x0,x1,idx} intervals
function computeEnvelope() {
const W = cv.width;
// critical x values: endpoints + intersections
const xs = new Set();
xs.add(0); xs.add(W);
segments.forEach(s => { xs.add(s.x1); xs.add(s.x2); });
// add all pairwise intersection x-coordinates
for (let i = 0; i < segments.length; i++) {
for (let j = i + 1; j < segments.length; j++) {
const xi = intersectX(segments[i], segments[j]);
if (xi !== null && xi > 0 && xi < W) xs.add(xi);
}
}
const sortedX = [...xs].sort((a, b) => a - b);
// sweep intervals, track which segment is minimum
const pieces = [];
let prevMin = null;
for (let k = 0; k < sortedX.length - 1; k++) {
const xMid = (sortedX[k] + sortedX[k + 1]) / 2;
let minY = Infinity, minIdx = -1;
segments.forEach((s, i) => {
if (xMid >= s.x1 && xMid <= s.x2) {
const y = segY(s, xMid);
if (y < minY) { minY = y; minIdx = i; }
}
});
if (minIdx !== -1) {
if (prevMin !== minIdx) {
pieces.push({ x0: sortedX[k], x1: sortedX[k + 1], idx: minIdx });
prevMin = minIdx;
} else {
pieces[pieces.length - 1].x1 = sortedX[k + 1];
}
} else {
prevMin = null;
}
}
return pieces;
}
// x-coordinate of intersection of two non-vertical lines, or null
function intersectX(a, b) {
const dya = a.y2 - a.y1, dxa = a.x2 - a.x1;
const dyb = b.y2 - b.y1, dxb = b.x2 - b.x1;
if (dxa === 0 || dxb === 0) return null;
const ma = dya / dxa, mb = dyb / dxb;
if (Math.abs(ma - mb) < 1e-10) return null;
const ba = a.y1 - ma * a.x1, bb = b.y1 - mb * b.x1;
return (bb - ba) / (ma - mb);
}
function draw() {
const W = cv.width, H = cv.height;
ctx.clearRect(0, 0, W, H);
// draw all segments faintly
segments.forEach((s, i) => {
ctx.strokeStyle = COLORS[i % COLORS.length];
ctx.lineWidth = 1.5;
ctx.globalAlpha = 0.55;
ctx.beginPath(); ctx.moveTo(s.x1, s.y1); ctx.lineTo(s.x2, s.y2); ctx.stroke();
ctx.globalAlpha = 1;
[{ x: s.x1, y: s.y1 }, { x: s.x2, y: s.y2 }].forEach(p => {
ctx.fillStyle = COLORS[i % COLORS.length];
ctx.beginPath(); ctx.arc(p.x, p.y, 3.5, 0, 2 * Math.PI); ctx.fill();
});
});
// draw the pending first endpoint
if (pending) {
ctx.strokeStyle = '#aaa';
ctx.setLineDash([4, 3]);
ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(pending.x, pending.y, 4, 0, 2 * Math.PI); ctx.stroke();
ctx.setLineDash([]);
}
// draw the lower envelope thickly
if (segments.length > 0) {
const pieces = computeEnvelope();
ctx.lineWidth = 3;
ctx.globalAlpha = 0.95;
pieces.forEach(p => {
const s = segments[p.idx];
ctx.strokeStyle = COLORS[p.idx % COLORS.length];
const y0 = segY(s, p.x0), y1 = segY(s, p.x1);
ctx.beginPath(); ctx.moveTo(p.x0, y0); ctx.lineTo(p.x1, y1); ctx.stroke();
});
ctx.globalAlpha = 1;
document.getElementById('env-count').textContent = pieces.length;
} else {
document.getElementById('env-count').textContent = 0;
}
document.getElementById('seg-count').textContent = segments.length;
}
function updateStatus() {
const n = segments.length;
if (n === 0) {
document.getElementById('status').textContent = 'Click anywhere to place the first endpoint of a segment.';
} else if (pending) {
document.getElementById('status').textContent = 'Now click a second point to complete the segment.';
} else {
const pieces = computeEnvelope().length;
const worst = n * n;
document.getElementById('status').textContent =
fmt('Envelope has %pieces% pieces for %n% segments — naïve worst case would be n² = %worst%.', { pieces: pieces, n: n, worst: worst });
}
}
cv.addEventListener('click', e => {
const rect = cv.getBoundingClientRect();
const scaleX = cv.width / rect.width;
const scaleY = cv.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
if (!pending) {
pending = { x, y };
} else {
if (Math.abs(x - pending.x) > 5) {
// normalize so x1 <= x2
const seg = pending.x < x
? { x1: pending.x, y1: pending.y, x2: x, y2: y }
: { x1: x, y1: y, x2: pending.x, y2: pending.y };
segments.push(seg);
}
pending = null;
}
draw();
updateStatus();
});
document.getElementById('btn-reset').addEventListener('click', () => {
segments.length = 0;
pending = null;
draw();
updateStatus();
});
draw();
updateStatus();
Comments
Loading comments...