Files
illustrated-algorithm/gcd/index.html
T
2026-08-23 14:50:31 +00:00

521 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>欧几里得算法图解</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: #f0f4f8;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 900px;
width: 100%;
background: #fff;
border-radius: 16px;
padding: 28px 30px 20px;
box-shadow: 0 8px 30px rgba(0,0,0,.08);
}
h1 {
text-align: center;
margin: 0 0 4px;
color: #2c3e50;
}
.subtitle {
text-align: center;
color: #7f8c8d;
margin: 0 0 18px;
font-size: 16px;
}
code {
background: #ecf0f1;
padding: 2px 8px;
border-radius: 4px;
font-family: 'Cascadia Mono', Consolas, monospace;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: center;
align-items: center;
margin-bottom: 10px;
}
.controls label {
font-size: 16px;
color: #2c3e50;
}
.controls input {
width: 90px;
padding: 7px 10px;
font-size: 16px;
border: 2px solid #dce1e6;
border-radius: 8px;
text-align: center;
transition: border-color .2s;
}
.controls input:focus {
outline: none;
border-color: #3498db;
}
button {
padding: 8px 18px;
border: none;
border-radius: 8px;
background: #3498db;
color: #fff;
font-size: 14px;
cursor: pointer;
transition: background .2s, opacity .2s;
}
button:hover {
background: #2980b9;
}
button.secondary {
background: #95a5a6;
}
button.secondary:hover {
background: #7f8c8d;
}
button:disabled {
opacity: .5;
cursor: not-allowed;
}
.sub-nav {
display: flex;
gap: 12px;
justify-content: center;
margin: 0 0 8px;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 18px;
justify-content: center;
margin: 8px 0 4px;
font-size: 14px;
color: #34495e;
}
.legend span {
display: inline-flex;
align-items: center;
gap: 6px;
}
.swatch {
display: inline-block;
width: 18px;
height: 18px;
border-radius: 4px;
border: 1px solid rgba(0,0,0,.15);
}
.info {
text-align: center;
min-height: 24px;
font-size: 16px;
color: #34495e;
margin: 10px 0 4px;
}
.equation {
text-align: center;
font-size: 24px;
font-family: 'Cascadia Mono', Consolas, monospace;
color: #c0392b;
margin: 6px 0 10px;
}
.eq-note {
font-size: 14px;
color: #7f8c8d;
}
.canvas-wrap {
text-align: center;
}
canvas {
max-width: 100%;
height: auto;
border: 1px solid #e8ecf0;
border-radius: 12px;
background: #fbfcfd;
}
.note {
text-align: center;
color: #95a5a6;
font-size: 13px;
margin-top: 8px;
}
.result {
text-align: center;
font-size: 22px;
color: #27ae60;
margin: 12px 0 6px;
font-weight: bold;
}
.steps {
position: relative;
background: #fafbfc;
border: 1px solid #eef1f4;
border-radius: 10px;
padding: 10px 12px;
margin-top: 14px;
max-height: 220px;
overflow-y: auto;
font-size: 15px;
line-height: 1.7;
}
.step {
padding: 5px 10px;
border-left: 4px solid #dfe6ea;
border-radius: 0 6px 6px 0;
margin: 5px 0;
background: #fff;
}
.step.active {
background: #eef6fd;
border-left-color: #3498db;
}
.step.swap {
color: #7f8c8d;
border-left-color: #f39c12;
background: #fef9f2;
}
.step-eq {
color: #7f8c8d;
font-family: monospace;
font-size: 13px;
margin-left: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>欧几里得算法图解</h1>
<p class="subtitle">核心公式:<code>gcd(a, b) = gcd(b, a mod b)</code></p>
<div class="controls">
<label>a = <input id="num1" type="number" min="1" step="1" value="24"></label>
<label>b = <input id="num2" type="number" min="1" step="1" value="18"></label>
<button onclick="start()">开始演示</button>
<button id="autoBtn" class="secondary" onclick="toggleAuto()">自动演示</button>
<button class="secondary" onclick="randomExample()">随机示例</button>
</div>
<div class="sub-nav">
<button id="prevBtn" onclick="prev()">◀ 上一步</button>
<button id="nextBtn" onclick="next()">下一步 ▶</button>
</div>
<div class="legend">
<span><span class="swatch" style="background:rgba(52,152,219,.6);"></span> 边长为当前较小数的正方形,共 q 个</span>
<span><span class="swatch" style="background:rgba(243,156,18,.75);"></span> 余数带,旋转后成为下一步的矩形</span>
</div>
<div id="info" class="info"></div>
<div id="equation" class="equation"></div>
<div class="canvas-wrap">
<canvas id="canvas" width="760" height="460"></canvas>
</div>
<div class="note">示意图按同一比例缩放;橙色余数条旋转 90° 后就是下一步要切的矩形。</div>
<div id="result" class="result" style="display:none;"></div>
<div id="steps" class="steps"></div>
</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let steps = [];
let current = 0;
let timer = null;
let originalA = 0;
let originalB = 0;
// 生成欧几里得算法的每一步
function computeSteps(a, b) {
let m = Math.max(a, b);
let n = Math.min(a, b);
const arr = [];
while (n > 0) {
const q = Math.floor(m / n);
const r = m % n;
arr.push({ m, n, q, r });
m = n;
n = r;
}
return arr;
}
function stopAuto() {
if (timer) {
clearInterval(timer);
timer = null;
document.getElementById('autoBtn').textContent = '自动演示';
}
}
function start() {
const a = parseInt(document.getElementById('num1').value, 10);
const b = parseInt(document.getElementById('num2').value, 10);
if (!Number.isInteger(a) || !Number.isInteger(b) || a <= 0 || b <= 0) {
alert('请输入正整数!');
return;
}
originalA = a;
originalB = b;
steps = computeSteps(a, b);
current = 0;
stopAuto();
update();
}
function next() {
stopAuto();
if (current < steps.length - 1) {
current++;
update();
}
}
function prev() {
stopAuto();
if (current > 0) {
current--;
update();
}
}
function toggleAuto() {
if (timer) {
stopAuto();
return;
}
if (steps.length === 0) start();
if (steps.length === 0) return;
if (current >= steps.length - 1) {
current = 0;
update();
}
document.getElementById('autoBtn').textContent = '暂停';
timer = setInterval(() => {
if (current < steps.length - 1) {
current++;
update();
} else {
stopAuto();
}
}, 2000);
}
function update() {
if (steps.length === 0) return;
const step = steps[current];
const total = steps.length;
drawStep(step);
const info = document.getElementById('info');
const equation = document.getElementById('equation');
const result = document.getElementById('result');
if (step.r === 0) {
info.innerHTML = `第 ${current + 1} / ${total} 步:余数为 0,图中已经全是边长为 <b>${step.n}</b> 的正方形。`;
equation.innerHTML = `gcd(${step.m}, ${step.n}) = <b>${step.n}</b> <span class="eq-note">(${step.m} = ${step.q} × ${step.n} + 0)</span>`;
result.innerHTML = `✅ gcd(${originalA}, ${originalB}) = <b>${step.n}</b>`;
result.style.display = 'block';
} else {
info.innerHTML = `第 ${current + 1} / ${total} 步:从 ${step.m} × ${step.n} 的矩形中,最多切出 <b>${step.q}</b> 个边长为 <b>${step.n}</b> 的正方形,剩余 <b>${step.r}</b> × ${step.n} 的橙色余条。`;
equation.innerHTML = `gcd(${step.m}, ${step.n}) = gcd(${step.n}, ${step.r}) <span class="eq-note">(${step.m} = ${step.q} × ${step.n} + ${step.r})</span>`;
result.style.display = 'none';
}
document.getElementById('prevBtn').disabled = (current === 0);
document.getElementById('nextBtn').disabled = (current === total - 1);
renderSteps();
}
function renderSteps() {
const stepsDiv = document.getElementById('steps');
let html = '';
// 如果输入时 a < b,为了画成“较宽矩形”,先交换说明
if (originalA && originalB && originalA !== originalB && originalA < originalB) {
html += `<div class="step swap">交换:gcd(${originalA}, ${originalB}) = gcd(${originalB}, ${originalA})(方便画图)</div>`;
}
steps.forEach((s, i) => {
const cls = 'step' + (i === current ? ' active' : '');
if (s.r === 0) {
html += `<div class="${cls}"><b>第${i + 1}步</b>:gcd(${s.m}, ${s.n}) = ${s.n}<span class="step-eq">${s.m} = ${s.q} × ${s.n} + 0</span></div>`;
} else {
html += `<div class="${cls}"><b>第${i + 1}步</b>:gcd(${s.m}, ${s.n}) = gcd(${s.n}, ${s.r})<span class="step-eq">${s.m} = ${s.q} × ${s.n} + ${s.r}</span></div>`;
}
});
stepsDiv.innerHTML = html;
const active = stepsDiv.querySelector('.active');
if (active) {
const top = active.offsetTop;
const bottom = top + active.offsetHeight;
if (top < stepsDiv.scrollTop || bottom > stepsDiv.scrollTop + stepsDiv.clientHeight) {
stepsDiv.scrollTop = top - (stepsDiv.clientHeight - active.offsetHeight) / 2;
}
}
}
// 绘制当前步骤的矩形图解
function drawStep(step) {
const { m, n, q, r } = step;
const W = canvas.width;
const H = canvas.height;
ctx.clearRect(0, 0, W, H);
const left = 70, right = 50, top = 20, bottom = 55;
const availW = W - left - right;
const availH = H - top - bottom;
const scale = Math.min(availW / m, availH / n);
const rectW = m * scale;
const rectH = n * scale;
const x = (W - rectW) / 2;
const y = (H - rectH) / 2;
ctx.save();
// 外矩形背景
ctx.fillStyle = '#f4f7f9';
ctx.strokeStyle = '#2c3e50';
ctx.lineWidth = 2;
ctx.fillRect(x, y, rectW, rectH);
ctx.strokeRect(x, y, rectW, rectH);
const squareSize = n * scale;
const maxDraw = 60;
// 绘制蓝色正方形
if (q <= maxDraw) {
ctx.fillStyle = 'rgba(52,152,219,0.6)';
ctx.strokeStyle = 'rgba(46,134,193,0.8)';
ctx.lineWidth = 1;
for (let i = 0; i < q; i++) {
ctx.fillRect(x + i * squareSize, y, squareSize, squareSize);
ctx.strokeRect(x + i * squareSize, y, squareSize, squareSize);
}
} else {
// 如果方块太多,只画两端的一部分,中间用文字表示
const leftCount = 15;
const rightCount = 15;
ctx.fillStyle = 'rgba(52,152,219,0.6)';
ctx.strokeStyle = 'rgba(46,134,193,0.8)';
ctx.lineWidth = 1;
for (let i = 0; i < leftCount; i++) {
ctx.fillRect(x + i * squareSize, y, squareSize, squareSize);
ctx.strokeRect(x + i * squareSize, y, squareSize, squareSize);
}
for (let i = q - rightCount; i < q; i++) {
ctx.fillRect(x + i * squareSize, y, squareSize, squareSize);
ctx.strokeRect(x + i * squareSize, y, squareSize, squareSize);
}
const midStart = x + leftCount * squareSize;
const midWidth = (q - leftCount - rightCount) * squareSize;
ctx.fillStyle = 'rgba(52,152,219,0.2)';
ctx.fillRect(midStart, y, midWidth, rectH);
ctx.fillStyle = '#2c3e50';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`共 ${q} 个`, x + (q / 2) * squareSize, y + rectH / 2);
}
// 绘制橙色余数带
if (r > 0) {
const rx = x + q * squareSize;
const rw = r * scale;
ctx.fillStyle = 'rgba(243,156,18,0.75)';
ctx.strokeStyle = 'rgba(230,126,34,0.9)';
ctx.lineWidth = 1.5;
ctx.fillRect(rx, y, rw, rectH);
ctx.strokeRect(rx, y, rw, rectH);
if (rw > 35 && rectH > 35) {
ctx.fillStyle = '#6e4a00';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`r=${r}`, rx + rw / 2, y + rectH / 2);
}
}
// 重新描边,让外框更清晰
ctx.strokeStyle = '#2c3e50';
ctx.lineWidth = 2;
ctx.strokeRect(x, y, rectW, rectH);
// 尺寸标注
ctx.fillStyle = '#2c3e50';
ctx.font = 'bold 15px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'alphabetic';
ctx.fillText(`m = ${m}`, x + rectW / 2, y + rectH + 28);
ctx.save();
ctx.translate(x - 28, y + rectH / 2);
ctx.rotate(-Math.PI / 2);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`n = ${n}`, 0, 0);
ctx.restore();
ctx.restore();
}
// 随机示例
function randomExample() {
const examples = [
[1071, 462],
[24, 18],
[180, 48],
[98, 56],
[55, 34],
[240, 126],
[13, 7],
[625, 125],
[97, 13],
[31415, 27182],
[64, 36],
[119, 34]
];
const [a, b] = examples[Math.floor(Math.random() * examples.length)];
document.getElementById('num1').value = a;
document.getElementById('num2').value = b;
start();
}
// 页面加载后默认演示 24 和 18
window.addEventListener('DOMContentLoaded', () => {
document.getElementById('num1').value = 24;
document.getElementById('num2').value = 18;
start();
});
</script>
</body>
</html>