Files
illustrated-algorithm/gcd.html
T

521 lines
13 KiB
HTML
Raw Normal View History

<!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>