Below is a live snake evolving on a synthetic blob. The circle of control points starts outside the object and shrinks toward its boundary, driven by image edge forces and an internal smoothness penalty.
<!-- Snake active-contour demo — canvas + controls -->
<div class="top-bar">
<span class="label">Energy: <strong id="energy-val">—</strong></span>
<span class="label">Iteration: <strong id="iter-val">0</strong></span>
</div>
<canvas id="canvas" width="380" height="300" title="Active contour snake evolving around a blob"></canvas>
<div class="status" id="status">Press Step or Run to start the snake.</div>
<div class="btns">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Styles for the active-contour snake demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.top-bar { display: flex; gap: 1.4rem; margin-bottom: .4rem; font-size: .85rem; color: #555; }
.label strong { color: #1d3557; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; background: #f0f4f8;
max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .5rem 0; }
.status.running { color: #1d7a55; }
.status.done { color: #0a7d33; }
.status.idle { color: #555; }
.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: .45; cursor: default; }
/* Active-contour snake demo — pure vanilla JS, no imports */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Blob shape definition (irregular polygon via interpolated radii) */
const CX = 190, CY = 150; /* canvas center */
const BLOB_RADII = [80, 70, 90, 75, 85, 95, 72, 88]; /* pixel radius at 8 evenly-spaced angles — creates an irregular blob */
function blobRadius(angle) {
const n = BLOB_RADII.length;
const t = ((angle % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
const idx = (t / (2 * Math.PI)) * n;
const i0 = Math.floor(idx) % n, i1 = (i0 + 1) % n;
const frac = idx - Math.floor(idx);
/* linear interpolation between anchor radii */
return BLOB_RADII[i0] * (1 - frac) + BLOB_RADII[i1] * frac;
}
/* Snake parameters (alpha=elasticity, beta=rigidity, gamma=step size) */
const N = 40; /* number of snake control points */
const ALPHA = 0.4; /* elasticity — resists stretching */
const BETA = 0.3; /* rigidity — resists bending */
const GAMMA = 8; /* step-size denominator (larger = smaller steps) */
const MAX_ITER = 120;
/* Pre-compute a Gaussian edge-response field and its gradient */
const W = 380, H = 300;
let gradX, gradY;
function buildEdgeField() {
/* Gaussian bump centered on the blob boundary (acts as image edge energy) */
const img = new Float32Array(W * H);
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
const dx = x - CX, dy = y - CY;
const angle = Math.atan2(dy, dx);
const r = Math.sqrt(dx * dx + dy * dy);
const br = blobRadius(angle);
/* soft distance-to-boundary; peaks at the blob edge */
img[y * W + x] = Math.exp(-((r - br) * (r - br)) / 150);
}
}
/* central-difference gradient of the edge field */
gradX = new Float32Array(W * H);
gradY = new Float32Array(W * H);
for (let y = 1; y < H - 1; y++) {
for (let x = 1; x < W - 1; x++) {
gradX[y * W + x] = (img[y * W + (x + 1)] - img[y * W + (x - 1)]) / 2;
gradY[y * W + x] = (img[(y + 1) * W + x] - img[(y - 1) * W + x]) / 2;
}
}
}
function sampleGrad(x, y) {
/* clamp coordinates and look up pre-computed gradient */
const xi = Math.max(1, Math.min(W - 2, Math.round(x)));
const yi = Math.max(1, Math.min(H - 2, Math.round(y)));
return [gradX[yi * W + xi], gradY[yi * W + xi]];
}
/* Snake state — array of [x, y] control points */
let pts = [];
function initSnake() {
/* Start with a circle of radius 130 around the blob center */
const R = 130;
pts = [];
for (let i = 0; i < N; i++) {
const a = (2 * Math.PI * i) / N;
pts.push([CX + R * Math.cos(a), CY + R * Math.sin(a)]);
}
}
function computeEnergy() {
/* Compute discrete snake energy (internal + image terms) */
let e = 0;
for (let i = 0; i < N; i++) {
const prev = pts[(i - 1 + N) % N], cur = pts[i], next = pts[(i + 1) % N];
/* elasticity term (first-difference squared) */
const dx = cur[0] - prev[0], dy = cur[1] - prev[1];
e += ALPHA * (dx * dx + dy * dy);
/* rigidity term (second-difference squared) */
const bx = next[0] - 2 * cur[0] + prev[0], by = next[1] - 2 * cur[1] + prev[1];
e += BETA * (bx * bx + by * by);
/* image term (negative gradient magnitude squared, scaled) */
const [gx, gy] = sampleGrad(cur[0], cur[1]);
e -= (gx * gx + gy * gy) * 10;
}
return e;
}
function stepSnake() {
/* One gradient-descent step — update every control point */
const newPts = pts.map((p, i) => {
const prev = pts[(i - 1 + N) % N], next = pts[(i + 1) % N];
const pp = pts[(i - 2 + N) % N], nn = pts[(i + 2) % N];
/* internal elastic + rigid forces (finite-difference approximation) */
const fx = ALPHA * (prev[0] + next[0] - 2 * p[0])
- BETA * (pp[0] - 4 * prev[0] + 6 * p[0] - 4 * next[0] + nn[0]);
const fy = ALPHA * (prev[1] + next[1] - 2 * p[1])
- BETA * (pp[1] - 4 * prev[1] + 6 * p[1] - 4 * next[1] + nn[1]);
/* external image force from pre-computed gradient field */
const [gx, gy] = sampleGrad(p[0], p[1]);
return [
p[0] + (fx + gx) / GAMMA,
p[1] + (fy + gy) / GAMMA
];
});
pts = newPts;
}
/* Canvas rendering */
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
function drawBlob() {
/* draw the blob in light blue */
ctx.beginPath();
for (let i = 0; i <= 360; i++) {
const a = (i * Math.PI) / 180;
const r = blobRadius(a);
const x = CX + r * Math.cos(a), y = CY + r * Math.sin(a);
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fillStyle = '#b8d4f0';
ctx.fill();
ctx.strokeStyle = '#4a7cb5';
ctx.lineWidth = 2;
ctx.stroke();
}
function drawSnake() {
/* draw the snake contour in red */
ctx.beginPath();
ctx.moveTo(pts[0][0], pts[0][1]);
for (let i = 1; i < N; i++) ctx.lineTo(pts[i][0], pts[i][1]);
ctx.closePath();
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 2.2;
ctx.stroke();
/* draw control points as filled circles */
for (const [x, y] of pts) {
ctx.beginPath();
ctx.arc(x, y, 3, 0, 2 * Math.PI);
ctx.fillStyle = '#e63946';
ctx.fill();
}
}
function draw() {
ctx.clearRect(0, 0, W, H);
drawBlob();
drawSnake();
}
/* Animation state and UI logic */
let iter = 0, running = false, rafId = null;
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg; el.className = 'status ' + cls;
}
function updateInfo() {
document.getElementById('iter-val').textContent = iter;
document.getElementById('energy-val').textContent = computeEnergy().toFixed(1);
}
function reset() {
/* Cancel any animation and reinitialize the snake */
running = false;
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
iter = 0;
initSnake();
draw();
updateInfo();
setStatus('Press Step or Run to start the snake.', 'idle');
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = false;
document.getElementById('btn-run').disabled = false;
}
function doStep() {
if (iter >= MAX_ITER) return false;
stepSnake();
iter++;
draw();
updateInfo();
return iter < MAX_ITER;
}
function step() {
if (iter >= MAX_ITER) return;
doStep();
if (iter >= MAX_ITER) {
setStatus(i18nFmt('Converged in %n% iterations.', { n: iter }), 'done');
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-run').disabled = true;
} else {
setStatus(i18nFmt('Iteration %n% — converging...', { n: iter }), 'idle');
}
}
function runLoop() {
if (!running) return;
const more = doStep();
setStatus(i18nFmt('Iteration %n% — converging...', { n: iter }), 'running');
if (!more) {
running = false;
setStatus(i18nFmt('Converged in %n% iterations.', { n: iter }), 'done');
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-run').disabled = true;
return;
}
rafId = requestAnimationFrame(runLoop);
}
function toggleRun() {
if (iter >= MAX_ITER) return;
running = !running;
document.getElementById('btn-run').textContent = running ? 'Pause' : 'Run';
document.getElementById('btn-step').disabled = running;
if (running) runLoop();
else if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-run').addEventListener('click', toggleRun);
document.getElementById('btn-reset').addEventListener('click', reset);
/* Bootstrap — build the edge field and initialize the snake */
buildEdgeField();
reset();
<!-- Snake active-contour demo — canvas + controls -->
<div class="top-bar">
<span class="label">Energy: <strong id="energy-val">—</strong></span>
<span class="label">Iteration: <strong id="iter-val">0</strong></span>
</div>
<canvas id="canvas" width="380" height="300" title="Active contour snake evolving around a blob"></canvas>
<div class="status" id="status">Press Step or Run to start the snake.</div>
<div class="btns">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Styles for the active-contour snake demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.top-bar { display: flex; gap: 1.4rem; margin-bottom: .4rem; font-size: .85rem; color: #555; }
.label strong { color: #1d3557; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; background: #f0f4f8;
max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .5rem 0; }
.status.running { color: #1d7a55; }
.status.done { color: #0a7d33; }
.status.idle { color: #555; }
.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: .45; cursor: default; }
/* Active-contour snake demo — pure vanilla JS, no imports */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Blob shape definition (irregular polygon via interpolated radii) */
const CX = 190, CY = 150; /* canvas center */
const BLOB_RADII = [80, 70, 90, 75, 85, 95, 72, 88]; /* pixel radius at 8 evenly-spaced angles — creates an irregular blob */
function blobRadius(angle) {
const n = BLOB_RADII.length;
const t = ((angle % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
const idx = (t / (2 * Math.PI)) * n;
const i0 = Math.floor(idx) % n, i1 = (i0 + 1) % n;
const frac = idx - Math.floor(idx);
/* linear interpolation between anchor radii */
return BLOB_RADII[i0] * (1 - frac) + BLOB_RADII[i1] * frac;
}
/* Snake parameters (alpha=elasticity, beta=rigidity, gamma=step size) */
const N = 40; /* number of snake control points */
const ALPHA = 0.4; /* elasticity — resists stretching */
const BETA = 0.3; /* rigidity — resists bending */
const GAMMA = 8; /* step-size denominator (larger = smaller steps) */
const MAX_ITER = 120;
/* Pre-compute a Gaussian edge-response field and its gradient */
const W = 380, H = 300;
let gradX, gradY;
function buildEdgeField() {
/* Gaussian bump centered on the blob boundary (acts as image edge energy) */
const img = new Float32Array(W * H);
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
const dx = x - CX, dy = y - CY;
const angle = Math.atan2(dy, dx);
const r = Math.sqrt(dx * dx + dy * dy);
const br = blobRadius(angle);
/* soft distance-to-boundary; peaks at the blob edge */
img[y * W + x] = Math.exp(-((r - br) * (r - br)) / 150);
}
}
/* central-difference gradient of the edge field */
gradX = new Float32Array(W * H);
gradY = new Float32Array(W * H);
for (let y = 1; y < H - 1; y++) {
for (let x = 1; x < W - 1; x++) {
gradX[y * W + x] = (img[y * W + (x + 1)] - img[y * W + (x - 1)]) / 2;
gradY[y * W + x] = (img[(y + 1) * W + x] - img[(y - 1) * W + x]) / 2;
}
}
}
function sampleGrad(x, y) {
/* clamp coordinates and look up pre-computed gradient */
const xi = Math.max(1, Math.min(W - 2, Math.round(x)));
const yi = Math.max(1, Math.min(H - 2, Math.round(y)));
return [gradX[yi * W + xi], gradY[yi * W + xi]];
}
/* Snake state — array of [x, y] control points */
let pts = [];
function initSnake() {
/* Start with a circle of radius 130 around the blob center */
const R = 130;
pts = [];
for (let i = 0; i < N; i++) {
const a = (2 * Math.PI * i) / N;
pts.push([CX + R * Math.cos(a), CY + R * Math.sin(a)]);
}
}
function computeEnergy() {
/* Compute discrete snake energy (internal + image terms) */
let e = 0;
for (let i = 0; i < N; i++) {
const prev = pts[(i - 1 + N) % N], cur = pts[i], next = pts[(i + 1) % N];
/* elasticity term (first-difference squared) */
const dx = cur[0] - prev[0], dy = cur[1] - prev[1];
e += ALPHA * (dx * dx + dy * dy);
/* rigidity term (second-difference squared) */
const bx = next[0] - 2 * cur[0] + prev[0], by = next[1] - 2 * cur[1] + prev[1];
e += BETA * (bx * bx + by * by);
/* image term (negative gradient magnitude squared, scaled) */
const [gx, gy] = sampleGrad(cur[0], cur[1]);
e -= (gx * gx + gy * gy) * 10;
}
return e;
}
function stepSnake() {
/* One gradient-descent step — update every control point */
const newPts = pts.map((p, i) => {
const prev = pts[(i - 1 + N) % N], next = pts[(i + 1) % N];
const pp = pts[(i - 2 + N) % N], nn = pts[(i + 2) % N];
/* internal elastic + rigid forces (finite-difference approximation) */
const fx = ALPHA * (prev[0] + next[0] - 2 * p[0])
- BETA * (pp[0] - 4 * prev[0] + 6 * p[0] - 4 * next[0] + nn[0]);
const fy = ALPHA * (prev[1] + next[1] - 2 * p[1])
- BETA * (pp[1] - 4 * prev[1] + 6 * p[1] - 4 * next[1] + nn[1]);
/* external image force from pre-computed gradient field */
const [gx, gy] = sampleGrad(p[0], p[1]);
return [
p[0] + (fx + gx) / GAMMA,
p[1] + (fy + gy) / GAMMA
];
});
pts = newPts;
}
/* Canvas rendering */
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
function drawBlob() {
/* draw the blob in light blue */
ctx.beginPath();
for (let i = 0; i <= 360; i++) {
const a = (i * Math.PI) / 180;
const r = blobRadius(a);
const x = CX + r * Math.cos(a), y = CY + r * Math.sin(a);
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fillStyle = '#b8d4f0';
ctx.fill();
ctx.strokeStyle = '#4a7cb5';
ctx.lineWidth = 2;
ctx.stroke();
}
function drawSnake() {
/* draw the snake contour in red */
ctx.beginPath();
ctx.moveTo(pts[0][0], pts[0][1]);
for (let i = 1; i < N; i++) ctx.lineTo(pts[i][0], pts[i][1]);
ctx.closePath();
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 2.2;
ctx.stroke();
/* draw control points as filled circles */
for (const [x, y] of pts) {
ctx.beginPath();
ctx.arc(x, y, 3, 0, 2 * Math.PI);
ctx.fillStyle = '#e63946';
ctx.fill();
}
}
function draw() {
ctx.clearRect(0, 0, W, H);
drawBlob();
drawSnake();
}
/* Animation state and UI logic */
let iter = 0, running = false, rafId = null;
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg; el.className = 'status ' + cls;
}
function updateInfo() {
document.getElementById('iter-val').textContent = iter;
document.getElementById('energy-val').textContent = computeEnergy().toFixed(1);
}
function reset() {
/* Cancel any animation and reinitialize the snake */
running = false;
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
iter = 0;
initSnake();
draw();
updateInfo();
setStatus('Press Step or Run to start the snake.', 'idle');
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = false;
document.getElementById('btn-run').disabled = false;
}
function doStep() {
if (iter >= MAX_ITER) return false;
stepSnake();
iter++;
draw();
updateInfo();
return iter < MAX_ITER;
}
function step() {
if (iter >= MAX_ITER) return;
doStep();
if (iter >= MAX_ITER) {
setStatus(i18nFmt('Converged in %n% iterations.', { n: iter }), 'done');
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-run').disabled = true;
} else {
setStatus(i18nFmt('Iteration %n% — converging...', { n: iter }), 'idle');
}
}
function runLoop() {
if (!running) return;
const more = doStep();
setStatus(i18nFmt('Iteration %n% — converging...', { n: iter }), 'running');
if (!more) {
running = false;
setStatus(i18nFmt('Converged in %n% iterations.', { n: iter }), 'done');
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-run').disabled = true;
return;
}
rafId = requestAnimationFrame(runLoop);
}
function toggleRun() {
if (iter >= MAX_ITER) return;
running = !running;
document.getElementById('btn-run').textContent = running ? 'Pause' : 'Run';
document.getElementById('btn-step').disabled = running;
if (running) runLoop();
else if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-run').addEventListener('click', toggleRun);
document.getElementById('btn-reset').addEventListener('click', reset);
/* Bootstrap — build the edge field and initialize the snake */
buildEdgeField();
reset();
Comments
Loading comments...