<!-- Firefly Algorithm demo — canvas shows the fitness landscape and firefly positions -->
<div class="toolbar">
<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>
<span class="gen-label">Generation: <span id="gen-num">0</span></span>
</div>
<canvas id="canvas" width="480" height="320" title="Firefly optimization landscape"></canvas>
<div id="status" class="status"></div>
<p class="hint">Each dot is a firefly. Brighter dots sit on higher peaks. Click Step to advance one generation — each firefly moves toward any brighter neighbour, then takes a small random step.</p>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: #fff; }
.toolbar { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
.gen-label { font-size: .85rem; color: #555; margin-left: .4rem; }
canvas { display: block; border: 1px solid #d0d8e0; border-radius: 8px; max-width: 100%; }
.status { font-size: .9rem; font-weight: 600; margin: .4rem 0 .3rem; min-height: 1.3em; color: #1d3557; }
.hint { font-size: .82rem; color: #555; line-height: 1.5; margin-top: .25rem; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Fitness landscape defined as a sum of Gaussian bumps.
// Peak definitions: {x, y} = position (0-1), h = height, w = spread.
const PEAKS = [
{ x: 0.22, y: 0.30, h: 1.0, w: 0.18 },
{ x: 0.72, y: 0.25, h: 0.85, w: 0.15 },
{ x: 0.50, y: 0.68, h: 0.70, w: 0.20 },
{ x: 0.15, y: 0.78, h: 0.45, w: 0.12 },
{ x: 0.85, y: 0.72, h: 0.55, w: 0.14 }
];
// fitness(x, y) = sum of Gaussian contributions from all peaks.
function fitness(px, py) {
let v = 0;
for (const p of PEAKS) {
const dx = px - p.x, dy = py - p.y;
v += p.h * Math.exp(-(dx * dx + dy * dy) / (2 * p.w * p.w));
}
return v;
}
// Firefly Algorithm parameters.
const N = 28; // number of fireflies
const ALPHA = 0.15; // random walk step size
const BETA0 = 1.0; // maximum attractiveness at distance 0
const GAMMA = 2.0; // light absorption coefficient
let flies = [], gen = 0, runTimer = null;
function initFlies() {
// Initialise fireflies at random positions across the unit square.
flies = Array.from({ length: N }, () => {
const x = Math.random(), y = Math.random();
return { x, y, f: fitness(x, y) };
});
gen = 0;
}
// One generation: move each firefly toward all brighter neighbours, then add noise.
function step() {
const next = flies.map(fi => {
let nx = fi.x, ny = fi.y;
for (const fj of flies) {
if (fj.f <= fi.f) continue; // only move toward strictly brighter fireflies
const r2 = (fi.x - fj.x) ** 2 + (fi.y - fj.y) ** 2;
const beta = BETA0 * Math.exp(-GAMMA * r2);
nx += beta * (fj.x - fi.x);
ny += beta * (fj.y - fi.y);
}
// Add random perturbation to avoid premature convergence.
nx += ALPHA * (Math.random() - 0.5);
ny += ALPHA * (Math.random() - 0.5);
nx = Math.max(0, Math.min(1, nx));
ny = Math.max(0, Math.min(1, ny));
return { x: nx, y: ny, f: fitness(nx, ny) };
});
flies = next;
gen++;
}
// Rendering: heatmap of the fitness landscape + firefly dots.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
function drawBackground() {
// Draw the background fitness heatmap (blue-dark to yellow).
const res = 40;
const cw = W / res, ch = H / res;
for (let i = 0; i < res; i++) {
for (let j = 0; j < res; j++) {
const fx = (i + 0.5) / res, fy = (j + 0.5) / res;
const v = fitness(fx, fy);
const t = Math.min(1, v / 1.1);
const r = Math.round(20 + t * 180);
const g = Math.round(30 + t * 160);
const b = Math.round(80 + t * 80);
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(i * cw, j * ch, cw + 1, ch + 1);
}
}
}
function drawFlies() {
// Draw each firefly as a circle whose size and colour encode fitness.
const best = Math.max(...flies.map(f => f.f));
for (const f of flies) {
const cx = f.x * W, cy = f.y * H;
const t = f.f / 1.1;
const r = 4 + t * 4;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, 2 * Math.PI);
const alpha = 0.6 + t * 0.4;
const g = Math.round(200 + t * 55);
ctx.fillStyle = `rgba(255,${g},0,${alpha})`;
ctx.fill();
if (f.f === best) {
// Highlight the best firefly with a white ring.
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
}
}
}
function draw() {
ctx.clearRect(0, 0, W, H);
drawBackground();
drawFlies();
}
function updateStatus() {
const best = Math.max(...flies.map(f => f.f));
document.getElementById('status').textContent =
i18nFmt('Generation %gen% — best fitness: %best%', { gen, best: best.toFixed(3) });
document.getElementById('gen-num').textContent = gen;
}
function doStep() {
step();
draw();
updateStatus();
}
document.getElementById('btn-step').addEventListener('click', doStep);
document.getElementById('btn-run').addEventListener('click', function () {
if (runTimer) {
// Pause the run.
clearInterval(runTimer);
runTimer = null;
this.textContent = 'Run';
document.getElementById('btn-step').disabled = false;
} else {
// Start continuous animation.
this.textContent = 'Pause';
document.getElementById('btn-step').disabled = true;
runTimer = setInterval(doStep, 120);
}
});
document.getElementById('btn-reset').addEventListener('click', function () {
if (runTimer) {
clearInterval(runTimer);
runTimer = null;
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = false;
}
initFlies();
draw();
updateStatus();
});
// Initialise and paint the first frame.
initFlies();
draw();
updateStatus();
<!-- Firefly Algorithm demo — canvas shows the fitness landscape and firefly positions -->
<div class="toolbar">
<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>
<span class="gen-label">Generation: <span id="gen-num">0</span></span>
</div>
<canvas id="canvas" width="480" height="320" title="Firefly optimization landscape"></canvas>
<div id="status" class="status"></div>
<p class="hint">Each dot is a firefly. Brighter dots sit on higher peaks. Click Step to advance one generation — each firefly moves toward any brighter neighbour, then takes a small random step.</p>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: #fff; }
.toolbar { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
.gen-label { font-size: .85rem; color: #555; margin-left: .4rem; }
canvas { display: block; border: 1px solid #d0d8e0; border-radius: 8px; max-width: 100%; }
.status { font-size: .9rem; font-weight: 600; margin: .4rem 0 .3rem; min-height: 1.3em; color: #1d3557; }
.hint { font-size: .82rem; color: #555; line-height: 1.5; margin-top: .25rem; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Fitness landscape defined as a sum of Gaussian bumps.
// Peak definitions: {x, y} = position (0-1), h = height, w = spread.
const PEAKS = [
{ x: 0.22, y: 0.30, h: 1.0, w: 0.18 },
{ x: 0.72, y: 0.25, h: 0.85, w: 0.15 },
{ x: 0.50, y: 0.68, h: 0.70, w: 0.20 },
{ x: 0.15, y: 0.78, h: 0.45, w: 0.12 },
{ x: 0.85, y: 0.72, h: 0.55, w: 0.14 }
];
// fitness(x, y) = sum of Gaussian contributions from all peaks.
function fitness(px, py) {
let v = 0;
for (const p of PEAKS) {
const dx = px - p.x, dy = py - p.y;
v += p.h * Math.exp(-(dx * dx + dy * dy) / (2 * p.w * p.w));
}
return v;
}
// Firefly Algorithm parameters.
const N = 28; // number of fireflies
const ALPHA = 0.15; // random walk step size
const BETA0 = 1.0; // maximum attractiveness at distance 0
const GAMMA = 2.0; // light absorption coefficient
let flies = [], gen = 0, runTimer = null;
function initFlies() {
// Initialise fireflies at random positions across the unit square.
flies = Array.from({ length: N }, () => {
const x = Math.random(), y = Math.random();
return { x, y, f: fitness(x, y) };
});
gen = 0;
}
// One generation: move each firefly toward all brighter neighbours, then add noise.
function step() {
const next = flies.map(fi => {
let nx = fi.x, ny = fi.y;
for (const fj of flies) {
if (fj.f <= fi.f) continue; // only move toward strictly brighter fireflies
const r2 = (fi.x - fj.x) ** 2 + (fi.y - fj.y) ** 2;
const beta = BETA0 * Math.exp(-GAMMA * r2);
nx += beta * (fj.x - fi.x);
ny += beta * (fj.y - fi.y);
}
// Add random perturbation to avoid premature convergence.
nx += ALPHA * (Math.random() - 0.5);
ny += ALPHA * (Math.random() - 0.5);
nx = Math.max(0, Math.min(1, nx));
ny = Math.max(0, Math.min(1, ny));
return { x: nx, y: ny, f: fitness(nx, ny) };
});
flies = next;
gen++;
}
// Rendering: heatmap of the fitness landscape + firefly dots.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
function drawBackground() {
// Draw the background fitness heatmap (blue-dark to yellow).
const res = 40;
const cw = W / res, ch = H / res;
for (let i = 0; i < res; i++) {
for (let j = 0; j < res; j++) {
const fx = (i + 0.5) / res, fy = (j + 0.5) / res;
const v = fitness(fx, fy);
const t = Math.min(1, v / 1.1);
const r = Math.round(20 + t * 180);
const g = Math.round(30 + t * 160);
const b = Math.round(80 + t * 80);
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(i * cw, j * ch, cw + 1, ch + 1);
}
}
}
function drawFlies() {
// Draw each firefly as a circle whose size and colour encode fitness.
const best = Math.max(...flies.map(f => f.f));
for (const f of flies) {
const cx = f.x * W, cy = f.y * H;
const t = f.f / 1.1;
const r = 4 + t * 4;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, 2 * Math.PI);
const alpha = 0.6 + t * 0.4;
const g = Math.round(200 + t * 55);
ctx.fillStyle = `rgba(255,${g},0,${alpha})`;
ctx.fill();
if (f.f === best) {
// Highlight the best firefly with a white ring.
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
}
}
}
function draw() {
ctx.clearRect(0, 0, W, H);
drawBackground();
drawFlies();
}
function updateStatus() {
const best = Math.max(...flies.map(f => f.f));
document.getElementById('status').textContent =
i18nFmt('Generation %gen% — best fitness: %best%', { gen, best: best.toFixed(3) });
document.getElementById('gen-num').textContent = gen;
}
function doStep() {
step();
draw();
updateStatus();
}
document.getElementById('btn-step').addEventListener('click', doStep);
document.getElementById('btn-run').addEventListener('click', function () {
if (runTimer) {
// Pause the run.
clearInterval(runTimer);
runTimer = null;
this.textContent = 'Run';
document.getElementById('btn-step').disabled = false;
} else {
// Start continuous animation.
this.textContent = 'Pause';
document.getElementById('btn-step').disabled = true;
runTimer = setInterval(doStep, 120);
}
});
document.getElementById('btn-reset').addEventListener('click', function () {
if (runTimer) {
clearInterval(runTimer);
runTimer = null;
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = false;
}
initFlies();
draw();
updateStatus();
});
// Initialise and paint the first frame.
initFlies();
draw();
updateStatus();
After a few steps you will see clusters forming around the tallest peaks. Some fireflies get trapped on local maxima — the random perturbation sometimes frees them, but not always. That tension between exploitation (climbing your current hill) and exploration (jumping to an unknown peak) is the central challenge of every metaheuristic.
Comments
Loading comments...