<div class="controls">
<label for="alpha-slider">Alpha (ฮฑ): <span id="alpha-val">0.05</span></label>
<input type="range" id="alpha-slider" min="0" max="100" value="5" step="1">
<span class="hint-label" id="shape-label">Convex hull</span>
</div>
<canvas id="canvas" width="460" height="320"></canvas>
<p class="hint">Click on the canvas to add points. Drag the slider to change ฮฑ โ the rolling-disk radius is <b>r = 1/ฮฑ</b>. At ฮฑ โ 0 the disk is infinite: convex hull. As ฮฑ grows the boundary tucks into concavities.</p>
<div class="btns">
<button id="btn-preset" type="button">Load example</button>
<button id="btn-clear" type="button" class="ghost">Clear</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #fff; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .4rem; }
label { font-size: .9rem; font-weight: 600; white-space: nowrap; }
input[type=range] { flex: 1; min-width: 120px; max-width: 260px; accent-color: #1d3557; cursor: pointer; }
.hint-label { font-size: .82rem; color: #1d3557; font-weight: 700; min-width: 110px; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; background: #f4f7fa;
cursor: crosshair; touch-action: none; max-width: 100%; }
.hint { font-size: .84rem; color: #555; margin: .45rem 0; line-height: 1.45; }
.btns { display: flex; gap: .5rem; }
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] ?? ''));
// Alpha shapes via Delaunay triangulation:
// A triangle/edge belongs to the alpha complex if its circumradius <= 1/alpha.
// At alpha=0 (infinite disk) => convex hull. As alpha grows, boundary tucks in.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const slider = document.getElementById('alpha-slider');
const alphaVal = document.getElementById('alpha-val');
const shapeLabel = document.getElementById('shape-label');
let pts = [];
// โโ Geometry helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function circumcircle(ax, ay, bx, by, cx, cy) {
const dx = bx - ax, dy = by - ay;
const ex = cx - ax, ey = cy - ay;
const bl = dx * dx + dy * dy;
const cl = ex * ex + ey * ey;
const d = 0.5 / (dx * ey - dy * ex);
if (Math.abs(d) > 1e9) return null; // degenerate
const ux = (ey * bl - dy * cl) * d;
const uy = (dx * cl - ex * bl) * d;
return { cx: ax + ux, cy: ay + uy, r: Math.sqrt(ux * ux + uy * uy) };
}
function edgeCircumradius(ax, ay, bx, by) {
// The smallest circle through two points has diameter = distance.
return Math.sqrt((bx - ax) ** 2 + (by - ay) ** 2) / 2;
}
// โโ BowyerโWatson Delaunay triangulation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function superTriangle(pts) {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const [x, y] of pts) {
if (x < minX) minX = x; if (y < minY) minY = y;
if (x > maxX) maxX = x; if (y > maxY) maxY = y;
}
const dx = maxX - minX, dy = maxY - minY;
const delta = Math.max(dx, dy) * 10;
const mx = (minX + maxX) / 2, my = (minY + maxY) / 2;
return [
[mx - 20 * delta, my - delta],
[mx, my + 20 * delta],
[mx + 20 * delta, my - delta]
];
}
function delaunay(points) {
if (points.length < 3) return [];
const st = superTriangle(points);
const all = [...points, ...st];
const idx = points.length; // super-triangle vertex indices start here
let triangles = [[idx, idx + 1, idx + 2]];
for (let pi = 0; pi < points.length; pi++) {
const [px, py] = points[pi];
const badTri = [];
const polygon = [];
for (const tri of triangles) {
const [a, b, c] = tri;
const cc = circumcircle(all[a][0], all[a][1], all[b][0], all[b][1], all[c][0], all[c][1]);
if (!cc) continue;
const dx = px - cc.cx, dy = py - cc.cy;
if (dx * dx + dy * dy <= cc.r * cc.r + 1e-10) {
badTri.push(tri);
polygon.push([a, b], [b, c], [c, a]);
}
}
// Remove bad triangles
for (const bt of badTri) {
const i = triangles.indexOf(bt);
if (i !== -1) triangles.splice(i, 1);
}
// Find boundary edges of the polygon hole
const boundary = polygon.filter(e =>
polygon.filter(f => (f[0] === e[1] && f[1] === e[0])).length === 0
);
for (const [a, b] of boundary) {
triangles.push([a, b, pi]);
}
}
// Remove triangles that share a vertex with the super-triangle
return triangles.filter(([a, b, c]) => a < idx && b < idx && c < idx);
}
// โโ Alpha shape extraction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function alphaShape(pts, alpha) {
if (pts.length === 0) return { edges: [], triangles: [] };
if (pts.length === 1) return { edges: [], triangles: [], vertices: [0] };
const r = alpha <= 0 ? Infinity : 1 / alpha;
if (pts.length === 2) {
const d = edgeCircumradius(pts[0][0], pts[0][1], pts[1][0], pts[1][1]);
return { edges: d <= r ? [[0, 1]] : [], triangles: [] };
}
const tris = delaunay(pts);
const edgeSet = new Set();
const boundaryEdges = new Map(); // edge -> count of adjacent triangles in alpha complex
const includedTris = [];
for (const [a, b, c] of tris) {
const cc = circumcircle(pts[a][0], pts[a][1], pts[b][0], pts[b][1], pts[c][0], pts[c][1]);
if (!cc) continue;
const inComplex = cc.r <= r;
if (inComplex) {
includedTris.push([a, b, c]);
for (const [p, q] of [[a, b], [b, c], [c, a]]) {
const key = p < q ? p + ',' + q : q + ',' + p;
boundaryEdges.set(key, (boundaryEdges.get(key) || 0) + 1);
}
} else {
// Try edges of this triangle independently
for (const [p, q] of [[a, b], [b, c], [c, a]]) {
const er = edgeCircumradius(pts[p][0], pts[p][1], pts[q][0], pts[q][1]);
if (er <= r) {
const key = p < q ? p + ',' + q : q + ',' + p;
edgeSet.add(key);
}
}
}
}
// Boundary edges of alpha complex = edges belonging to exactly one triangle
const boundary = [];
for (const [key, count] of boundaryEdges) {
if (count === 1) boundary.push(key.split(',').map(Number));
}
// Add isolated edges
for (const key of edgeSet) {
if (!boundaryEdges.has(key)) boundary.push(key.split(',').map(Number));
}
return { edges: boundary, triangles: includedTris };
}
// โโ Convex hull (for label) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function convexHullEdgeCount(pts) {
if (pts.length < 3) return pts.length;
// Just need the count for label; use Jarvis march
let start = 0;
for (let i = 1; i < pts.length; i++) if (pts[i][0] < pts[start][0]) start = i;
let current = start, hull = 0;
do {
let next = (current + 1) % pts.length;
for (let i = 0; i < pts.length; i++) {
const ax = pts[next][0] - pts[current][0], ay = pts[next][1] - pts[current][1];
const bx = pts[i][0] - pts[current][0], by = pts[i][1] - pts[current][1];
if (ax * by - ay * bx < 0) next = i;
}
current = next; hull++;
} while (current !== start && hull < pts.length + 1);
return hull;
}
// โโ Rendering โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function draw() {
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
if (pts.length === 0) {
ctx.fillStyle = '#aaa';
ctx.font = '14px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Click to add points', W / 2, H / 2);
return;
}
const sliderPct = parseInt(slider.value, 10); // 0..100
// alpha mapping: 0 -> 0 (convex hull), 100 -> 0.15
const alpha = sliderPct === 0 ? 0 : sliderPct * 0.0015;
const r = alpha <= 0 ? Infinity : 1 / alpha;
alphaVal.textContent = alpha === 0 ? '0 (โ)' : alpha.toFixed(4);
const shape = alphaShape(pts, alpha);
// Fill included triangles
ctx.fillStyle = 'rgba(29,53,87,0.12)';
for (const [a, b, c] of shape.triangles) {
ctx.beginPath();
ctx.moveTo(pts[a][0], pts[a][1]);
ctx.lineTo(pts[b][0], pts[b][1]);
ctx.lineTo(pts[c][0], pts[c][1]);
ctx.closePath();
ctx.fill();
}
// Draw boundary edges
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
for (const [a, b] of shape.edges) {
ctx.beginPath();
ctx.moveTo(pts[a][0], pts[a][1]);
ctx.lineTo(pts[b][0], pts[b][1]);
ctx.stroke();
}
// Draw points
for (const [x, y] of pts) {
ctx.beginPath();
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fillStyle = '#e63946';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
}
// Rolling disk indicator (top-right, capped for display)
if (alpha > 0 && r < 200) {
const dr = Math.min(r, 60);
ctx.beginPath();
ctx.arc(W - dr - 10, dr + 10, dr, 0, 2 * Math.PI);
ctx.strokeStyle = 'rgba(100,160,230,0.55)';
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 4]);
ctx.stroke();
ctx.setLineDash([]);
}
// Label
const edgeCount = shape.edges.length;
if (alpha === 0) {
shapeLabel.textContent = 'Convex hull';
} else if (edgeCount === 0) {
shapeLabel.textContent = 'Only vertices';
} else {
shapeLabel.textContent = fmt('Alpha complex (%edgeCount% edges)', { edgeCount: edgeCount });
}
}
// โโ Preset example (crescent shape) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function loadPreset() {
pts = [];
const W = canvas.width, H = canvas.height;
// Crescent / C-shape point cloud
const cx = W * 0.5, cy = H * 0.5;
const R = 110, r2 = 60;
for (let a = Math.PI * 0.15; a <= Math.PI * 1.85; a += Math.PI / 14) {
pts.push([cx + R * Math.cos(a) + (Math.random() - 0.5) * 12,
cy + R * Math.sin(a) + (Math.random() - 0.5) * 12]);
}
for (let a = Math.PI * 0.4; a <= Math.PI * 1.6; a += Math.PI / 10) {
pts.push([cx + r2 * Math.cos(a) + (Math.random() - 0.5) * 10,
cy + r2 * Math.sin(a) + (Math.random() - 0.5) * 10]);
}
slider.value = 30;
draw();
}
// โโ Events โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
pts.push([(e.clientX - rect.left) * scaleX, (e.clientY - rect.top) * scaleY]);
draw();
});
slider.addEventListener('input', draw);
document.getElementById('btn-preset').addEventListener('click', loadPreset);
document.getElementById('btn-clear').addEventListener('click', () => { pts = []; slider.value = 5; draw(); });
loadPreset();
<div class="controls">
<label for="alpha-slider">Alpha (ฮฑ): <span id="alpha-val">0.05</span></label>
<input type="range" id="alpha-slider" min="0" max="100" value="5" step="1">
<span class="hint-label" id="shape-label">Convex hull</span>
</div>
<canvas id="canvas" width="460" height="320"></canvas>
<p class="hint">Click on the canvas to add points. Drag the slider to change ฮฑ โ the rolling-disk radius is <b>r = 1/ฮฑ</b>. At ฮฑ โ 0 the disk is infinite: convex hull. As ฮฑ grows the boundary tucks into concavities.</p>
<div class="btns">
<button id="btn-preset" type="button">Load example</button>
<button id="btn-clear" type="button" class="ghost">Clear</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #fff; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .4rem; }
label { font-size: .9rem; font-weight: 600; white-space: nowrap; }
input[type=range] { flex: 1; min-width: 120px; max-width: 260px; accent-color: #1d3557; cursor: pointer; }
.hint-label { font-size: .82rem; color: #1d3557; font-weight: 700; min-width: 110px; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; background: #f4f7fa;
cursor: crosshair; touch-action: none; max-width: 100%; }
.hint { font-size: .84rem; color: #555; margin: .45rem 0; line-height: 1.45; }
.btns { display: flex; gap: .5rem; }
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] ?? ''));
// Alpha shapes via Delaunay triangulation:
// A triangle/edge belongs to the alpha complex if its circumradius <= 1/alpha.
// At alpha=0 (infinite disk) => convex hull. As alpha grows, boundary tucks in.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const slider = document.getElementById('alpha-slider');
const alphaVal = document.getElementById('alpha-val');
const shapeLabel = document.getElementById('shape-label');
let pts = [];
// โโ Geometry helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function circumcircle(ax, ay, bx, by, cx, cy) {
const dx = bx - ax, dy = by - ay;
const ex = cx - ax, ey = cy - ay;
const bl = dx * dx + dy * dy;
const cl = ex * ex + ey * ey;
const d = 0.5 / (dx * ey - dy * ex);
if (Math.abs(d) > 1e9) return null; // degenerate
const ux = (ey * bl - dy * cl) * d;
const uy = (dx * cl - ex * bl) * d;
return { cx: ax + ux, cy: ay + uy, r: Math.sqrt(ux * ux + uy * uy) };
}
function edgeCircumradius(ax, ay, bx, by) {
// The smallest circle through two points has diameter = distance.
return Math.sqrt((bx - ax) ** 2 + (by - ay) ** 2) / 2;
}
// โโ BowyerโWatson Delaunay triangulation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function superTriangle(pts) {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const [x, y] of pts) {
if (x < minX) minX = x; if (y < minY) minY = y;
if (x > maxX) maxX = x; if (y > maxY) maxY = y;
}
const dx = maxX - minX, dy = maxY - minY;
const delta = Math.max(dx, dy) * 10;
const mx = (minX + maxX) / 2, my = (minY + maxY) / 2;
return [
[mx - 20 * delta, my - delta],
[mx, my + 20 * delta],
[mx + 20 * delta, my - delta]
];
}
function delaunay(points) {
if (points.length < 3) return [];
const st = superTriangle(points);
const all = [...points, ...st];
const idx = points.length; // super-triangle vertex indices start here
let triangles = [[idx, idx + 1, idx + 2]];
for (let pi = 0; pi < points.length; pi++) {
const [px, py] = points[pi];
const badTri = [];
const polygon = [];
for (const tri of triangles) {
const [a, b, c] = tri;
const cc = circumcircle(all[a][0], all[a][1], all[b][0], all[b][1], all[c][0], all[c][1]);
if (!cc) continue;
const dx = px - cc.cx, dy = py - cc.cy;
if (dx * dx + dy * dy <= cc.r * cc.r + 1e-10) {
badTri.push(tri);
polygon.push([a, b], [b, c], [c, a]);
}
}
// Remove bad triangles
for (const bt of badTri) {
const i = triangles.indexOf(bt);
if (i !== -1) triangles.splice(i, 1);
}
// Find boundary edges of the polygon hole
const boundary = polygon.filter(e =>
polygon.filter(f => (f[0] === e[1] && f[1] === e[0])).length === 0
);
for (const [a, b] of boundary) {
triangles.push([a, b, pi]);
}
}
// Remove triangles that share a vertex with the super-triangle
return triangles.filter(([a, b, c]) => a < idx && b < idx && c < idx);
}
// โโ Alpha shape extraction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function alphaShape(pts, alpha) {
if (pts.length === 0) return { edges: [], triangles: [] };
if (pts.length === 1) return { edges: [], triangles: [], vertices: [0] };
const r = alpha <= 0 ? Infinity : 1 / alpha;
if (pts.length === 2) {
const d = edgeCircumradius(pts[0][0], pts[0][1], pts[1][0], pts[1][1]);
return { edges: d <= r ? [[0, 1]] : [], triangles: [] };
}
const tris = delaunay(pts);
const edgeSet = new Set();
const boundaryEdges = new Map(); // edge -> count of adjacent triangles in alpha complex
const includedTris = [];
for (const [a, b, c] of tris) {
const cc = circumcircle(pts[a][0], pts[a][1], pts[b][0], pts[b][1], pts[c][0], pts[c][1]);
if (!cc) continue;
const inComplex = cc.r <= r;
if (inComplex) {
includedTris.push([a, b, c]);
for (const [p, q] of [[a, b], [b, c], [c, a]]) {
const key = p < q ? p + ',' + q : q + ',' + p;
boundaryEdges.set(key, (boundaryEdges.get(key) || 0) + 1);
}
} else {
// Try edges of this triangle independently
for (const [p, q] of [[a, b], [b, c], [c, a]]) {
const er = edgeCircumradius(pts[p][0], pts[p][1], pts[q][0], pts[q][1]);
if (er <= r) {
const key = p < q ? p + ',' + q : q + ',' + p;
edgeSet.add(key);
}
}
}
}
// Boundary edges of alpha complex = edges belonging to exactly one triangle
const boundary = [];
for (const [key, count] of boundaryEdges) {
if (count === 1) boundary.push(key.split(',').map(Number));
}
// Add isolated edges
for (const key of edgeSet) {
if (!boundaryEdges.has(key)) boundary.push(key.split(',').map(Number));
}
return { edges: boundary, triangles: includedTris };
}
// โโ Convex hull (for label) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function convexHullEdgeCount(pts) {
if (pts.length < 3) return pts.length;
// Just need the count for label; use Jarvis march
let start = 0;
for (let i = 1; i < pts.length; i++) if (pts[i][0] < pts[start][0]) start = i;
let current = start, hull = 0;
do {
let next = (current + 1) % pts.length;
for (let i = 0; i < pts.length; i++) {
const ax = pts[next][0] - pts[current][0], ay = pts[next][1] - pts[current][1];
const bx = pts[i][0] - pts[current][0], by = pts[i][1] - pts[current][1];
if (ax * by - ay * bx < 0) next = i;
}
current = next; hull++;
} while (current !== start && hull < pts.length + 1);
return hull;
}
// โโ Rendering โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function draw() {
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
if (pts.length === 0) {
ctx.fillStyle = '#aaa';
ctx.font = '14px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Click to add points', W / 2, H / 2);
return;
}
const sliderPct = parseInt(slider.value, 10); // 0..100
// alpha mapping: 0 -> 0 (convex hull), 100 -> 0.15
const alpha = sliderPct === 0 ? 0 : sliderPct * 0.0015;
const r = alpha <= 0 ? Infinity : 1 / alpha;
alphaVal.textContent = alpha === 0 ? '0 (โ)' : alpha.toFixed(4);
const shape = alphaShape(pts, alpha);
// Fill included triangles
ctx.fillStyle = 'rgba(29,53,87,0.12)';
for (const [a, b, c] of shape.triangles) {
ctx.beginPath();
ctx.moveTo(pts[a][0], pts[a][1]);
ctx.lineTo(pts[b][0], pts[b][1]);
ctx.lineTo(pts[c][0], pts[c][1]);
ctx.closePath();
ctx.fill();
}
// Draw boundary edges
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
for (const [a, b] of shape.edges) {
ctx.beginPath();
ctx.moveTo(pts[a][0], pts[a][1]);
ctx.lineTo(pts[b][0], pts[b][1]);
ctx.stroke();
}
// Draw points
for (const [x, y] of pts) {
ctx.beginPath();
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fillStyle = '#e63946';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
}
// Rolling disk indicator (top-right, capped for display)
if (alpha > 0 && r < 200) {
const dr = Math.min(r, 60);
ctx.beginPath();
ctx.arc(W - dr - 10, dr + 10, dr, 0, 2 * Math.PI);
ctx.strokeStyle = 'rgba(100,160,230,0.55)';
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 4]);
ctx.stroke();
ctx.setLineDash([]);
}
// Label
const edgeCount = shape.edges.length;
if (alpha === 0) {
shapeLabel.textContent = 'Convex hull';
} else if (edgeCount === 0) {
shapeLabel.textContent = 'Only vertices';
} else {
shapeLabel.textContent = fmt('Alpha complex (%edgeCount% edges)', { edgeCount: edgeCount });
}
}
// โโ Preset example (crescent shape) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function loadPreset() {
pts = [];
const W = canvas.width, H = canvas.height;
// Crescent / C-shape point cloud
const cx = W * 0.5, cy = H * 0.5;
const R = 110, r2 = 60;
for (let a = Math.PI * 0.15; a <= Math.PI * 1.85; a += Math.PI / 14) {
pts.push([cx + R * Math.cos(a) + (Math.random() - 0.5) * 12,
cy + R * Math.sin(a) + (Math.random() - 0.5) * 12]);
}
for (let a = Math.PI * 0.4; a <= Math.PI * 1.6; a += Math.PI / 10) {
pts.push([cx + r2 * Math.cos(a) + (Math.random() - 0.5) * 10,
cy + r2 * Math.sin(a) + (Math.random() - 0.5) * 10]);
}
slider.value = 30;
draw();
}
// โโ Events โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
pts.push([(e.clientX - rect.left) * scaleX, (e.clientY - rect.top) * scaleY]);
draw();
});
slider.addEventListener('input', draw);
document.getElementById('btn-preset').addEventListener('click', loadPreset);
document.getElementById('btn-clear').addEventListener('click', () => { pts = []; slider.value = 5; draw(); });
loadPreset();
Notice the key transitions. When alpha is very small the disk is huge and nothing gets carved โ you see the convex hull. As alpha grows the boundary tucks into concavities. Past a critical value the shape may split, and at very high alpha only the points themselves remain. The same cloud, endlessly different readings depending on the scale of the probe.
Comments
Loading comments...