Python素数の篩ビジュアライザ
Pythonでエラトステネスの篩を計算し、1〜150の格子上で素数を順次点灯アニメーション。アルゴリズム学習用の可視化デモ。
外部ライブラリ: https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.js
ライブデモ
使用例(お題: アイドルグループ Sakura)
この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- Sakura:ファン番号ラッキー診断。Pythonの篩で素数=ラッキーナンバーを点灯 -->
<section class="sk-lucky" aria-label="Sakura ラッキーナンバー診断">
<header class="sk-lucky__head">
<span class="sk-lucky__petal">🌸</span>
<div>
<h1 class="sk-lucky__title">ファンクラブ ラッキーナンバー</h1>
<p class="sk-lucky__sub">あなたの会員番号、素数なら今月の“桜運”アップ♪</p>
</div>
<button id="run" class="sk-lucky__btn" disabled>診断スタート</button>
</header>
<div class="sk-lucky__board">
<div class="sk-lucky__grid" id="grid" aria-label="会員番号 1〜150"></div>
</div>
<p class="sk-lucky__count" id="count">診断エンジンを準備中…</p>
</section>
CSS
/* Sakura:ファンクラブ ラッキーナンバー診断 */
:root {
--pink: #ffd1e0;
--pink2: #ff9ec0;
--accent: #ff5e9c;
--gray: #f3f4f7;
--ink: #5b4a55;
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 400px;
display: grid;
place-items: center;
background: radial-gradient(130% 100% at 50% 0%, #fff, var(--pink) 80%);
font-family: "Hiragino Maru Gothic ProN", "Rounded Mplus 1c", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
color: var(--ink);
overflow: hidden;
}
.sk-lucky {
width: min(560px, 94vw);
padding: 16px 22px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.82);
box-shadow: 0 18px 44px rgba(255, 94, 156, 0.2);
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
}
/* ヘッダ */
.sk-lucky__head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.sk-lucky__petal { font-size: 22px; }
.sk-lucky__title { margin: 0; font-size: 16px; font-weight: 800; color: var(--accent); }
.sk-lucky__sub { margin: 2px 0 0; font-size: 11px; color: #a98699; }
.sk-lucky__btn {
margin-left: auto;
font: inherit;
font-size: 12.5px;
font-weight: 700;
padding: 9px 18px;
border: none;
border-radius: 999px;
cursor: pointer;
color: #fff;
background: linear-gradient(135deg, var(--pink2), var(--accent));
box-shadow: 0 8px 18px rgba(255, 94, 156, 0.36);
transition: transform 0.2s ease;
}
.sk-lucky__btn:disabled { opacity: 0.5; cursor: default; }
.sk-lucky__btn:not(:disabled):hover { transform: translateY(-2px); }
/* 番号ボード */
.sk-lucky__board {
border-radius: 14px;
background: #fff;
padding: 12px;
box-shadow: inset 0 0 0 1px var(--pink);
}
.sk-lucky__grid {
display: grid;
grid-template-columns: repeat(25, 1fr);
gap: 3px;
}
.sk-cell {
aspect-ratio: 1;
display: grid;
place-items: center;
border-radius: 4px;
font-size: 7.5px;
font-weight: 700;
color: #c9aebb;
background: var(--gray);
transition: transform 0.25s ease, background 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
}
.sk-cell.is-one { color: #d8b8c6; }
/* ラッキーナンバー(素数)点灯 */
.sk-cell.is-prime {
color: #fff;
background: linear-gradient(135deg, var(--pink2), var(--accent));
box-shadow: 0 0 8px rgba(255, 94, 156, 0.6);
transform: scale(1.18);
}
.sk-lucky__count {
margin: 12px 0 0;
font-size: 12px;
text-align: center;
color: #a98699;
}
.sk-lucky__count b { color: var(--accent); font-size: 15px; }
@media (prefers-reduced-motion: reduce) {
.sk-cell, .sk-lucky__btn { transition: none; }
}
JavaScript
// 要素取得(null安全)
const $ = (id) => document.getElementById(id);
const grid = $("grid"), countEl = $("count"), runBtn = $("run");
if (grid && countEl && runBtn) {
const N = 150; // 会員番号 1..150
let pyodide = null, cells = [];
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
// セルを構築(1回だけ)
function buildCells() {
const frag = document.createDocumentFragment();
cells = [];
for (let n = 1; n <= N; n++) {
const c = document.createElement("div");
c.className = "sk-cell" + (n === 1 ? " is-one" : "");
c.textContent = n;
frag.append(c);
cells.push(c);
}
grid.replaceChildren(frag);
}
// Pythonでエラトステネスの篩 → 素数(=ラッキーナンバー)配列
function sievePrimes() {
pyodide.globals.set("N", N);
return pyodide.runPython(`
N_ = N
is_lucky = [True] * (N_ + 1)
is_lucky[0] = is_lucky[1] = False
p = 2
while p * p <= N_:
if is_lucky[p]:
for m in range(p * p, N_ + 1, p):
is_lucky[m] = False
p += 1
[i for i in range(2, N_ + 1) if is_lucky[i]]
`).toJs();
}
// ラッキーナンバーを順次点灯(桜が咲くように)
async function run() {
runBtn.disabled = true;
cells.forEach((c) => c.classList.remove("is-prime"));
const primes = Array.from(sievePrimes());
if (reduce) {
primes.forEach((p) => cells[p - 1].classList.add("is-prime"));
} else {
for (const p of primes) {
cells[p - 1].classList.add("is-prime");
await new Promise((r) => setTimeout(r, 20)); // 1個ずつ点灯
}
}
countEl.innerHTML = `ラッキーナンバーは <b>${primes.length}</b> 個 (会員番号 1〜${N})`;
runBtn.disabled = false;
}
runBtn.addEventListener("click", run);
// Pyodide起動
(async () => {
buildCells();
try {
const mod = await import("https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.mjs");
pyodide = await mod.loadPyodide();
runBtn.disabled = false;
run();
} catch (e) {
countEl.textContent = "診断を読み込めませんでした";
}
})();
}
コード
HTML
<!-- Pythonでエラトステネスの篩を計算しグリッドで可視化するデモ -->
<main class="sieve" aria-label="Python素数の篩">
<header class="sieve__head">
<h1 class="sieve__title">エラトステネスの篩</h1>
<p class="sieve__sub">Python で 1〜N を篩い、素数を点灯</p>
</header>
<!-- 数の格子 -->
<div class="sieve__grid" id="grid" role="img" aria-label="素数グリッド"></div>
<footer class="sieve__foot">
<span class="sieve__count" id="count" aria-live="polite">— 個の素数</span>
<button id="run" class="sieve__btn" disabled>篩いを実行</button>
</footer>
</main>
CSS
:root {
--bg: #080b16;
--ink: #eaf0ff;
--muted: #8b97c0;
--prime: #ffd166;
--comp: #1a2138;
--accent: #7c9cff;
--sans: system-ui, "Segoe UI", "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 14px;
font-family: var(--sans);
color: var(--ink);
background:
radial-gradient(700px 360px at 50% -10%, #16204a 0%, transparent 60%),
var(--bg);
}
.sieve {
width: min(100%, 560px);
background: rgba(14,18,38,.7);
border: 1px solid #232c52;
border-radius: 16px;
padding: 18px;
box-shadow: 0 26px 60px -26px rgba(0,0,0,.8);
backdrop-filter: blur(8px);
}
.sieve__head { text-align: center; margin-bottom: 16px; }
.sieve__title {
margin: 0; font-size: 18px; font-weight: 800;
background: linear-gradient(90deg, #ffd166, #7c9cff);
-webkit-background-clip: text; background-clip: text; color: transparent;
}
.sieve__sub { margin: 4px 0 0; font-size: 11px; color: var(--muted); }
/* 数の格子 */
.sieve__grid {
display: grid;
grid-template-columns: repeat(15, 1fr);
gap: 4px;
margin-bottom: 16px;
}
.cell {
aspect-ratio: 1;
display: grid;
place-items: center;
font-size: 9px;
border-radius: 5px;
background: var(--comp);
color: #56608a;
transition: background .35s ease, color .35s ease, transform .35s ease, box-shadow .35s ease;
}
.cell.is-prime {
background: radial-gradient(circle at 50% 40%, #fff0c2, var(--prime));
color: #5a3d00;
font-weight: 700;
box-shadow: 0 0 10px -1px rgba(255,209,102,.7);
transform: scale(1.04);
}
.cell.is-one { background: #11162b; color: #3a4267; }
.sieve__foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.sieve__count { font-size: 13px; color: var(--ink); font-variant-numeric: tabular-nums; }
.sieve__count b { color: var(--prime); }
.sieve__btn {
font: inherit; font-size: 12px; font-weight: 700;
color: #0a0f20;
background: linear-gradient(180deg, #a9beff, var(--accent));
border: none; padding: 9px 18px; border-radius: 10px; cursor: pointer;
transition: transform .12s ease, box-shadow .12s ease;
}
.sieve__btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 8px 18px -6px var(--accent); }
.sieve__btn:disabled { opacity: .5; cursor: not-allowed; }
@media (prefers-reduced-motion: reduce) {
.cell { transition: background .01s, color .01s; transform: none !important; }
.sieve__btn { transition: none; }
}
JavaScript
// 要素取得(null安全)
const $ = (id) => document.getElementById(id);
const grid = $("grid"), countEl = $("count"), runBtn = $("run");
if (grid && countEl && runBtn) {
const N = 150; // 1..150 を表示
let pyodide = null, cells = [];
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
// セルを構築(1回だけ)
function buildCells() {
const frag = document.createDocumentFragment();
cells = [];
for (let n = 1; n <= N; n++) {
const c = document.createElement("div");
c.className = "cell" + (n === 1 ? " is-one" : "");
c.textContent = n;
frag.append(c);
cells.push(c);
}
grid.replaceChildren(frag);
}
// Pythonでエラトステネスの篩を計算 → 素数indexの配列
function sievePrimes() {
pyodide.globals.set("N", N);
return pyodide.runPython(`
N_ = N
is_prime = [True] * (N_ + 1)
is_prime[0] = is_prime[1] = False
p = 2
while p * p <= N_:
if is_prime[p]:
for m in range(p * p, N_ + 1, p):
is_prime[m] = False
p += 1
[i for i in range(2, N_ + 1) if is_prime[i]]
`).toJs();
}
// 素数を順次点灯(アニメーション)
async function run() {
runBtn.disabled = true;
// いったんリセット
cells.forEach((c) => c.classList.remove("is-prime"));
const primes = Array.from(sievePrimes());
if (reduce) {
primes.forEach((p) => cells[p - 1].classList.add("is-prime"));
} else {
for (const p of primes) {
cells[p - 1].classList.add("is-prime");
await new Promise((r) => setTimeout(r, 22)); // 1個ずつ点灯
}
}
countEl.innerHTML = `<b>${primes.length}</b> 個の素数 (1〜${N})`;
runBtn.disabled = false;
}
runBtn.addEventListener("click", run);
// Pyodide起動
(async () => {
buildCells();
try {
const mod = await import("https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.mjs");
pyodide = await mod.loadPyodide();
runBtn.disabled = false;
run();
} catch (e) {
countEl.textContent = "読込失敗: " + e.message;
}
})();
}
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「Python素数の篩ビジュアライザ」の効果を追加してください。
# 追加してほしい効果
Python素数の篩ビジュアライザ(Python (Pyodideブラウザ実行))
Pythonでエラトステネスの篩を計算し、1〜150の格子上で素数を順次点灯アニメーション。アルゴリズム学習用の可視化デモ。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- Pythonでエラトステネスの篩を計算しグリッドで可視化するデモ -->
<main class="sieve" aria-label="Python素数の篩">
<header class="sieve__head">
<h1 class="sieve__title">エラトステネスの篩</h1>
<p class="sieve__sub">Python で 1〜N を篩い、素数を点灯</p>
</header>
<!-- 数の格子 -->
<div class="sieve__grid" id="grid" role="img" aria-label="素数グリッド"></div>
<footer class="sieve__foot">
<span class="sieve__count" id="count" aria-live="polite">— 個の素数</span>
<button id="run" class="sieve__btn" disabled>篩いを実行</button>
</footer>
</main>
【CSS】
:root {
--bg: #080b16;
--ink: #eaf0ff;
--muted: #8b97c0;
--prime: #ffd166;
--comp: #1a2138;
--accent: #7c9cff;
--sans: system-ui, "Segoe UI", "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 14px;
font-family: var(--sans);
color: var(--ink);
background:
radial-gradient(700px 360px at 50% -10%, #16204a 0%, transparent 60%),
var(--bg);
}
.sieve {
width: min(100%, 560px);
background: rgba(14,18,38,.7);
border: 1px solid #232c52;
border-radius: 16px;
padding: 18px;
box-shadow: 0 26px 60px -26px rgba(0,0,0,.8);
backdrop-filter: blur(8px);
}
.sieve__head { text-align: center; margin-bottom: 16px; }
.sieve__title {
margin: 0; font-size: 18px; font-weight: 800;
background: linear-gradient(90deg, #ffd166, #7c9cff);
-webkit-background-clip: text; background-clip: text; color: transparent;
}
.sieve__sub { margin: 4px 0 0; font-size: 11px; color: var(--muted); }
/* 数の格子 */
.sieve__grid {
display: grid;
grid-template-columns: repeat(15, 1fr);
gap: 4px;
margin-bottom: 16px;
}
.cell {
aspect-ratio: 1;
display: grid;
place-items: center;
font-size: 9px;
border-radius: 5px;
background: var(--comp);
color: #56608a;
transition: background .35s ease, color .35s ease, transform .35s ease, box-shadow .35s ease;
}
.cell.is-prime {
background: radial-gradient(circle at 50% 40%, #fff0c2, var(--prime));
color: #5a3d00;
font-weight: 700;
box-shadow: 0 0 10px -1px rgba(255,209,102,.7);
transform: scale(1.04);
}
.cell.is-one { background: #11162b; color: #3a4267; }
.sieve__foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.sieve__count { font-size: 13px; color: var(--ink); font-variant-numeric: tabular-nums; }
.sieve__count b { color: var(--prime); }
.sieve__btn {
font: inherit; font-size: 12px; font-weight: 700;
color: #0a0f20;
background: linear-gradient(180deg, #a9beff, var(--accent));
border: none; padding: 9px 18px; border-radius: 10px; cursor: pointer;
transition: transform .12s ease, box-shadow .12s ease;
}
.sieve__btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 8px 18px -6px var(--accent); }
.sieve__btn:disabled { opacity: .5; cursor: not-allowed; }
@media (prefers-reduced-motion: reduce) {
.cell { transition: background .01s, color .01s; transform: none !important; }
.sieve__btn { transition: none; }
}
【JavaScript】
// 要素取得(null安全)
const $ = (id) => document.getElementById(id);
const grid = $("grid"), countEl = $("count"), runBtn = $("run");
if (grid && countEl && runBtn) {
const N = 150; // 1..150 を表示
let pyodide = null, cells = [];
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
// セルを構築(1回だけ)
function buildCells() {
const frag = document.createDocumentFragment();
cells = [];
for (let n = 1; n <= N; n++) {
const c = document.createElement("div");
c.className = "cell" + (n === 1 ? " is-one" : "");
c.textContent = n;
frag.append(c);
cells.push(c);
}
grid.replaceChildren(frag);
}
// Pythonでエラトステネスの篩を計算 → 素数indexの配列
function sievePrimes() {
pyodide.globals.set("N", N);
return pyodide.runPython(`
N_ = N
is_prime = [True] * (N_ + 1)
is_prime[0] = is_prime[1] = False
p = 2
while p * p <= N_:
if is_prime[p]:
for m in range(p * p, N_ + 1, p):
is_prime[m] = False
p += 1
[i for i in range(2, N_ + 1) if is_prime[i]]
`).toJs();
}
// 素数を順次点灯(アニメーション)
async function run() {
runBtn.disabled = true;
// いったんリセット
cells.forEach((c) => c.classList.remove("is-prime"));
const primes = Array.from(sievePrimes());
if (reduce) {
primes.forEach((p) => cells[p - 1].classList.add("is-prime"));
} else {
for (const p of primes) {
cells[p - 1].classList.add("is-prime");
await new Promise((r) => setTimeout(r, 22)); // 1個ずつ点灯
}
}
countEl.innerHTML = `<b>${primes.length}</b> 個の素数 (1〜${N})`;
runBtn.disabled = false;
}
runBtn.addEventListener("click", run);
// Pyodide起動
(async () => {
buildCells();
try {
const mod = await import("https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.mjs");
pyodide = await mod.loadPyodide();
runBtn.disabled = false;
run();
} catch (e) {
countEl.textContent = "読込失敗: " + e.message;
}
})();
}
# 外部ライブラリ
https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.js
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。