Every number starts unmarked. The smallest unmarked one is always prime. Click Step to advance the sieve one prime at a time — watch it cross out that prime's multiples. Or press Run to finish automatically.
<p class="hint">Watch the Sieve of Eratosthenes in action. The smallest unmarked number is always prime — the sieve crosses out all its multiples. Step through it one prime at a time or run it to completion.</p>
<div id="controls" class="btns">
<button id="stepBtn" type="button">Step</button>
<button id="runBtn" type="button">Run</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div class="info-row">
<span class="info" id="info">Click Step to start.</span>
<span class="count" id="count"></span>
</div>
<div id="grid" class="grid"></div>
<div class="legend">
<span class="leg-prime">prime</span>
<span class="leg-crossed">composite</span>
<span class="leg-current">current prime</span>
<span class="leg-unmarked">unmarked</span>
</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; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .55rem; }
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: .4; cursor: default; }
.info-row { display: flex; align-items: baseline; gap: .7rem; margin-bottom: .4rem; min-height: 1.4em; }
.info { font-size: .95rem; font-weight: 600; color: #1d3557; }
.count { font-size: .85rem; color: #555; }
.grid { display: grid; grid-template-columns: repeat(10, 1fr); gap: 4px; }
.cell { width: 100%; aspect-ratio: 1; display: flex; align-items: center;
justify-content: center; font: 600 13px ui-monospace, monospace;
border-radius: 6px; border: 1px solid #cdd9e3; background: #e8eef3;
color: #333; transition: background .2s, color .2s; }
.cell.unmarked { background: #e8eef3; color: #333; border-color: #cdd9e3; }
.cell.prime { background: #1d3557; color: #fff; border-color: #1d3557; }
.cell.current { background: #e63946; color: #fff; border-color: #c92f3c; }
.cell.crossed { background: #c9ccd1; color: #888; border-color: #adb1b8; text-decoration: line-through; }
.cell.skip { background: transparent; border-color: transparent; color: transparent; }
.legend { display: flex; gap: .7rem; flex-wrap: wrap; margin-top: .5rem; font-size: .8rem; }
.legend span { display: flex; align-items: center; gap: .3rem; }
.legend span::before { content: ""; display: inline-block; width: 12px; height: 12px; border-radius: 3px; }
.leg-prime::before { background: #1d3557; }
.leg-crossed::before { background: #c9ccd1; }
.leg-current::before { background: #e63946; }
.leg-unmarked::before { background: #e8eef3; border: 1px solid #cdd9e3; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Sieve of Eratosthenes demo — linear-sieve style (smallest prime factor)
// Complexity: O(n log log n) for the classic sieve displayed here.
// Each step: pick the smallest unmarked number (it's prime), mark all its
// multiples as composite. We stop at sqrt(N) for marking; remaining unmarked numbers are already prime.
const N = 100;
// state[i]: 'unmarked' | 'prime' | 'current' | 'crossed'
let state = [];
let nextPrime = 2; // next prime to sieve
let done = false;
let runTimer = null;
let primeCount = 0;
function init() {
state = Array(N + 1).fill('unmarked');
state[0] = state[1] = 'crossed'; // 0 and 1 are not prime
nextPrime = 2;
done = false;
primeCount = 0;
if (runTimer) { clearInterval(runTimer); runTimer = null; }
document.getElementById('stepBtn').disabled = false;
document.getElementById('runBtn').disabled = false;
setInfo('Click Step to start.', '');
render();
}
function render() {
const g = document.getElementById('grid');
g.innerHTML = '';
for (let i = 1; i <= N; i++) {
const d = document.createElement('div');
d.className = 'cell ' + state[i];
d.textContent = i;
g.appendChild(d);
}
document.getElementById('count').textContent =
primeCount > 0 ? fmt('%primeCount% prime%v% found', { primeCount: primeCount, v: primeCount > 1 ? 's' : '' }) : '';
}
function setInfo(msg, cnt) {
document.getElementById('info').textContent = msg;
if (cnt !== undefined) document.getElementById('count').textContent = cnt;
}
function step() {
if (done) return;
// Finalize the previous "current" prime — mark it as prime
for (let i = 2; i <= N; i++) {
if (state[i] === 'current') { state[i] = 'prime'; primeCount++; }
}
// Find next unmarked
while (nextPrime <= N && state[nextPrime] !== 'unmarked') nextPrime++;
if (nextPrime > N) {
// Any remaining unmarked numbers are prime (they survived all sieves)
for (let i = 2; i <= N; i++) {
if (state[i] === 'unmarked') { state[i] = 'prime'; primeCount++; }
}
done = true;
if (runTimer) { clearInterval(runTimer); runTimer = null; }
document.getElementById('stepBtn').disabled = true;
document.getElementById('runBtn').disabled = true;
setInfo(fmt('Done! All primes up to %N% found.', { N: N }), fmt('%primeCount% primes found', { primeCount: primeCount }));
render();
return;
}
const p = nextPrime;
state[p] = 'current';
// Cross out multiples of p (start at p*p — smaller multiples already crossed)
let crossed = 0;
for (let m = p * p; m <= N; m += p) {
if (state[m] === 'unmarked') { state[m] = 'crossed'; crossed++; }
}
// Also cross any multiples < p*p that may be unmarked (only relevant for first few primes)
for (let m = p * 2; m < p * p && m <= N; m += p) {
if (state[m] === 'unmarked') { state[m] = 'crossed'; crossed++; }
}
nextPrime = p + 1;
setInfo(fmt('Prime %p% — crossed out %crossed% new composite%v%.', { p: p, crossed: crossed, v: crossed !== 1 ? 's' : '' }), '');
render();
}
function run() {
document.getElementById('stepBtn').disabled = true;
document.getElementById('runBtn').disabled = true;
runTimer = setInterval(() => { step(); if (done) clearInterval(runTimer); }, 300);
}
document.getElementById('stepBtn').addEventListener('click', step);
document.getElementById('runBtn').addEventListener('click', run);
document.getElementById('resetBtn').addEventListener('click', init);
// Verify: after full sieve, primes up to 100 should be 25
// This is checked in STEP 3 externally.
init();
<p class="hint">Watch the Sieve of Eratosthenes in action. The smallest unmarked number is always prime — the sieve crosses out all its multiples. Step through it one prime at a time or run it to completion.</p>
<div id="controls" class="btns">
<button id="stepBtn" type="button">Step</button>
<button id="runBtn" type="button">Run</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div class="info-row">
<span class="info" id="info">Click Step to start.</span>
<span class="count" id="count"></span>
</div>
<div id="grid" class="grid"></div>
<div class="legend">
<span class="leg-prime">prime</span>
<span class="leg-crossed">composite</span>
<span class="leg-current">current prime</span>
<span class="leg-unmarked">unmarked</span>
</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; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .55rem; }
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: .4; cursor: default; }
.info-row { display: flex; align-items: baseline; gap: .7rem; margin-bottom: .4rem; min-height: 1.4em; }
.info { font-size: .95rem; font-weight: 600; color: #1d3557; }
.count { font-size: .85rem; color: #555; }
.grid { display: grid; grid-template-columns: repeat(10, 1fr); gap: 4px; }
.cell { width: 100%; aspect-ratio: 1; display: flex; align-items: center;
justify-content: center; font: 600 13px ui-monospace, monospace;
border-radius: 6px; border: 1px solid #cdd9e3; background: #e8eef3;
color: #333; transition: background .2s, color .2s; }
.cell.unmarked { background: #e8eef3; color: #333; border-color: #cdd9e3; }
.cell.prime { background: #1d3557; color: #fff; border-color: #1d3557; }
.cell.current { background: #e63946; color: #fff; border-color: #c92f3c; }
.cell.crossed { background: #c9ccd1; color: #888; border-color: #adb1b8; text-decoration: line-through; }
.cell.skip { background: transparent; border-color: transparent; color: transparent; }
.legend { display: flex; gap: .7rem; flex-wrap: wrap; margin-top: .5rem; font-size: .8rem; }
.legend span { display: flex; align-items: center; gap: .3rem; }
.legend span::before { content: ""; display: inline-block; width: 12px; height: 12px; border-radius: 3px; }
.leg-prime::before { background: #1d3557; }
.leg-crossed::before { background: #c9ccd1; }
.leg-current::before { background: #e63946; }
.leg-unmarked::before { background: #e8eef3; border: 1px solid #cdd9e3; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Sieve of Eratosthenes demo — linear-sieve style (smallest prime factor)
// Complexity: O(n log log n) for the classic sieve displayed here.
// Each step: pick the smallest unmarked number (it's prime), mark all its
// multiples as composite. We stop at sqrt(N) for marking; remaining unmarked numbers are already prime.
const N = 100;
// state[i]: 'unmarked' | 'prime' | 'current' | 'crossed'
let state = [];
let nextPrime = 2; // next prime to sieve
let done = false;
let runTimer = null;
let primeCount = 0;
function init() {
state = Array(N + 1).fill('unmarked');
state[0] = state[1] = 'crossed'; // 0 and 1 are not prime
nextPrime = 2;
done = false;
primeCount = 0;
if (runTimer) { clearInterval(runTimer); runTimer = null; }
document.getElementById('stepBtn').disabled = false;
document.getElementById('runBtn').disabled = false;
setInfo('Click Step to start.', '');
render();
}
function render() {
const g = document.getElementById('grid');
g.innerHTML = '';
for (let i = 1; i <= N; i++) {
const d = document.createElement('div');
d.className = 'cell ' + state[i];
d.textContent = i;
g.appendChild(d);
}
document.getElementById('count').textContent =
primeCount > 0 ? fmt('%primeCount% prime%v% found', { primeCount: primeCount, v: primeCount > 1 ? 's' : '' }) : '';
}
function setInfo(msg, cnt) {
document.getElementById('info').textContent = msg;
if (cnt !== undefined) document.getElementById('count').textContent = cnt;
}
function step() {
if (done) return;
// Finalize the previous "current" prime — mark it as prime
for (let i = 2; i <= N; i++) {
if (state[i] === 'current') { state[i] = 'prime'; primeCount++; }
}
// Find next unmarked
while (nextPrime <= N && state[nextPrime] !== 'unmarked') nextPrime++;
if (nextPrime > N) {
// Any remaining unmarked numbers are prime (they survived all sieves)
for (let i = 2; i <= N; i++) {
if (state[i] === 'unmarked') { state[i] = 'prime'; primeCount++; }
}
done = true;
if (runTimer) { clearInterval(runTimer); runTimer = null; }
document.getElementById('stepBtn').disabled = true;
document.getElementById('runBtn').disabled = true;
setInfo(fmt('Done! All primes up to %N% found.', { N: N }), fmt('%primeCount% primes found', { primeCount: primeCount }));
render();
return;
}
const p = nextPrime;
state[p] = 'current';
// Cross out multiples of p (start at p*p — smaller multiples already crossed)
let crossed = 0;
for (let m = p * p; m <= N; m += p) {
if (state[m] === 'unmarked') { state[m] = 'crossed'; crossed++; }
}
// Also cross any multiples < p*p that may be unmarked (only relevant for first few primes)
for (let m = p * 2; m < p * p && m <= N; m += p) {
if (state[m] === 'unmarked') { state[m] = 'crossed'; crossed++; }
}
nextPrime = p + 1;
setInfo(fmt('Prime %p% — crossed out %crossed% new composite%v%.', { p: p, crossed: crossed, v: crossed !== 1 ? 's' : '' }), '');
render();
}
function run() {
document.getElementById('stepBtn').disabled = true;
document.getElementById('runBtn').disabled = true;
runTimer = setInterval(() => { step(); if (done) clearInterval(runTimer); }, 300);
}
document.getElementById('stepBtn').addEventListener('click', step);
document.getElementById('runBtn').addEventListener('click', run);
document.getElementById('resetBtn').addEventListener('click', init);
// Verify: after full sieve, primes up to 100 should be 25
// This is checked in STEP 3 externally.
init();
Notice what the sieve never does: it never divides, never tests whether a number is divisible by anything. It only adds — jumping ahead by the prime's own size each time. A composite number gets crossed out the first time its smallest prime factor reaches it. That is why the linear sieve works: each composite is eliminated exactly once.
Comments
Loading comments...