Files
50ohm-contents-dl/contents/html/applet_nyquist.html
T

447 lines
12 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.
<html>
<style>
/* ---- Applet Card ---- */
.applet-card {
overflow: hidden;
}
.applet-header {
background: #f5f5f5;
padding: 12px 20px;
font-size: 14px;
font-weight: 500;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
}
.applet-header .material-icons { font-size: 18px; color: var(--primary); }
.applet-body { padding: 20px; }
/* ---- Canvas panels ---- */
.panel {
margin-bottom: 16px;
}
.panel-label {
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 6px;
}
.panel-label .dot {
width: 10px; height: 10px; border-radius: 50%;
display: inline-block;
}
canvas {
display: block;
width: 100%;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--canvas-bg);
}
/* ---- Controls ---- */
.controls {
background: #f9f9f9;
border: 1px solid var(--border);
border-radius: 6px;
padding: 16px 20px;
margin-bottom: 16px;
}
.control-row {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.control-value {
text-align: right;
}
.status-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-top: 12px;
}
.nyquist-info {
font-size: 13px;
color: var(--text-secondary);
}
/* ---- Info box ---- */
.info-box {
margin-top: 20px;
padding: 14px 18px;
background: "#DAEEFA";
border-left: 4px solid var(--primary);
border-radius: 0 4px 4px 0;
}
.info-box strong { font-weight: 700; }
/* ---- Legend ---- */
.legend {
display: flex;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 16px;
font-size: 13px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
}
.legend-line {
width: 24px; height: 3px; border-radius: 2px;
}
.legend-circle {
width: 10px; height: 10px; border-radius: 50%;
}
/* ---- Formula ---- */
.formula-box {
margin-top: 20px;
background: #263238;
color: #eceff1;
border-radius: 6px;
padding: 14px 18px;
font-size: 13px;
line-height: 1.8;
}
.formula-box .comment { color: #78909c; }
.formula-box .keyword { color: #80cbc4; }
.formula-box .func { color: #ffcc02; }
@media (max-width: 600px) {
.control-row { flex-direction: column; align-items: flex-start; }
.control-value { text-align: left; }
}
</style>
<div class="applet-card">
<div class="applet-body">
<!-- Controls -->
<div class="controls">
<div class="control-row">
<span class="control-label">Abtastrate $f_\text{s}$:</span>
<input type="range" id="sliderFs" min="1.0" max="8.0" step="0.025" value="3.0">
<span class="control-value" id="labelFs">3.0 kHz</span>
</div>
</div>
<!-- Panel 1: Original signal -->
<div class="panel">
<div class="panel-label">
<span class="dot" style="background:var(--signal-color)"></span>
Originalsignal — Sinus mit 1kHz
</div>
<canvas id="canvasOriginal" height="300"></canvas>
</div>
<!-- Panel 2: Sampled signal -->
<div class="panel">
<div class="panel-label">
<span class="dot" style="background:var(--sample-color)"></span>
Abgetastetes Signal
</div>
<canvas id="canvasSampled" height="300"></canvas>
</div>
<!-- Panel 3: Reconstructed signal -->
<div class="panel">
<div class="panel-label">
<span class="dot" style="background:var(--recon-color)"></span>
Rekonstruiertes Signal
</div>
<canvas id="canvasRecon" height="300"></canvas>
</div>
<!-- Info box -->
<div class="info-box" id="infoBox">
<strong>✓ Nyquist-Bedingung erfüllt:</strong>
f<sub>s</sub> = 3.0 kHz ≥ 2 · 1 kHz. Das rekonstruierte Signal stimmt mit dem Original überein.
</div><br>
</div>
</div>
</main>
<script>
// ============================================================
// Signal- und Darstellungsparameter
// ============================================================
const F_SIG = 1.0; // Signalfrequenz (normiert auf 1)
const T_SHOW = 3.0; // Angezeigte Zeitdauer in Perioden
const N_DRAW = 1000; // Stützpunkte für analoge Kurve
// Sinc-Funktion (normierte Version: sinc(u) = sin(πu)/(πu))
function sinc(u) {
if (Math.abs(u) < 1e-10) return 1.0;
return Math.sin(Math.PI * u) / (Math.PI * u);
}
// Originalsignal
function signal(t) {
return Math.sin(2 * Math.PI * F_SIG * t);
}
// Whittaker-Shannon-Rekonstruktion
function reconstruct(t, samples, fs) {
let sum = 0;
for (let n = 0; n < samples.length; n++) {
sum += samples[n].y * sinc(fs * t - n);
}
return sum;
}
// ============================================================
// Canvas-Hilfsklasse
// ============================================================
class Plot {
constructor(canvas, yMin=-1.3, yMax=1.3) {
this.canvas = canvas;
this.yMin = yMin;
this.yMax = yMax;
this.resize();
}
resize() {
const dpr = window.devicePixelRatio || 1;
const rect = this.canvas.getBoundingClientRect();
this.canvas.width = rect.width * dpr;
this.canvas.height = this.canvas.offsetHeight * dpr;
this.ctx = this.canvas.getContext('2d');
this.ctx.scale(dpr, dpr);
this.W = rect.width;
this.H = this.canvas.offsetHeight;
this.padL = 44; this.padR = 12;
this.padT = 10; this.padB = 22;
this.plotW = this.W - this.padL - this.padR;
this.plotH = this.H - this.padT - this.padB;
}
// map t∈[0,T_SHOW] to pixel x
tx(t) { return this.padL + (t / T_SHOW) * this.plotW; }
// map y∈[yMin,yMax] to pixel y
ty(y) { return this.padT + (1 - (y - this.yMin) / (this.yMax - this.yMin)) * this.plotH; }
clear() {
const ctx = this.ctx;
ctx.clearRect(0, 0, this.W, this.H);
// background
ctx.fillStyle = '#fafafa';
ctx.fillRect(this.padL, this.padT, this.plotW, this.plotH);
// grid lines
ctx.strokeStyle = '#e8e8e8';
ctx.lineWidth = 1;
for (let v of [-1, -0.5, 0, 0.5, 1]) {
const py = this.ty(v);
ctx.beginPath(); ctx.moveTo(this.padL, py); ctx.lineTo(this.padL + this.plotW, py);
ctx.stroke();
}
// time grid
for (let k = 0; k <= T_SHOW; k++) {
const px = this.tx(k);
ctx.beginPath(); ctx.moveTo(px, this.padT); ctx.lineTo(px, this.padT + this.plotH);
ctx.stroke();
}
// axes
ctx.strokeStyle = '#aaa';
ctx.lineWidth = 1;
ctx.strokeRect(this.padL, this.padT, this.plotW, this.plotH);
// zero line
ctx.strokeStyle = '#bbb';
ctx.lineWidth = 1.5;
const y0 = this.ty(0);
ctx.beginPath(); ctx.moveTo(this.padL, y0); ctx.lineTo(this.padL + this.plotW, y0);
ctx.stroke();
// y-axis labels
ctx.fillStyle = '#888';
ctx.textAlign = 'right';
for (let v of [-1, 0, 1]) {
ctx.fillText(v.toFixed(0), this.padL - 4, this.ty(v) + 3.5);
}
// x-axis labels
ctx.textAlign = 'center';
ctx.fillStyle = '#888';
for (let k = 0; k <= T_SHOW; k++) {
ctx.fillText(k + 'T', this.tx(k), this.padT + this.plotH + 14);
}
}
drawLine(points, color='#47ABE8', lw=2) {
const ctx = this.ctx;
ctx.strokeStyle = color;
ctx.lineWidth = lw;
ctx.lineJoin = 'round';
ctx.beginPath();
for (let i = 0; i < points.length; i++) {
const px = this.tx(points[i].t);
const py = this.ty(points[i].y);
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.stroke();
}
drawStems(samples, color='#FE756C') {
const ctx = this.ctx;
const y0 = this.ty(0);
// Draw vertical stem lines
ctx.strokeStyle = color;
ctx.lineWidth = 1.5;
for (const s of samples) {
if (s.t < 0 || s.t > T_SHOW) continue;
const px = this.tx(s.t);
const py = this.ty(s.y);
ctx.beginPath(); ctx.moveTo(px, y0); ctx.lineTo(px, py); ctx.stroke();
}
// Draw sample dots on top
ctx.fillStyle = color;
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
for (const s of samples) {
if (s.t < 0 || s.t > T_SHOW) continue;
const px = this.tx(s.t);
const py = this.ty(s.y);
ctx.beginPath();
ctx.arc(px, py, 4.5, 0, 2*Math.PI);
ctx.fill();
ctx.stroke();
}
}
}
// ============================================================
// Globale Plot-Objekte
// ============================================================
let plotOrig, plotSamp, plotRecon;
function initPlots() {
plotOrig = new Plot(document.getElementById('canvasOriginal'));
plotSamp = new Plot(document.getElementById('canvasSampled'));
plotRecon = new Plot(document.getElementById('canvasRecon'));
}
// ============================================================
// Haupt-Render-Funktion
// ============================================================
function render(fs) {
// --- 1. Abtastwerte erzeugen ---
const Ts = 1 / fs; // Abtastintervall (in Perioden des Signals)
const nSamples = Math.floor(T_SHOW * fs) + 2;
const samples = [];
for (let n = 0; n < nSamples; n++) {
const t = n * Ts;
samples.push({ t, y: signal(t) });
}
// --- 2. Analoge Kurve erzeugen ---
const analogPts = [];
for (let i = 0; i <= N_DRAW; i++) {
const t = (i / N_DRAW) * T_SHOW;
analogPts.push({ t, y: signal(t) });
}
// --- 3. Rekonstruktion (Sinc-Interpolation) ---
const reconPts = [];
for (let i = 0; i <= N_DRAW; i++) {
const t = (i / N_DRAW) * T_SHOW;
reconPts.push({ t, y: reconstruct(t, samples, fs) });
}
// --- 4. Panel 1: Original ---
plotOrig.clear();
plotOrig.drawLine(analogPts, '#47ABE8', 2.2);
// --- 5. Panel 2: Samples (analog + stems) ---
plotSamp.clear();
plotSamp.drawLine(analogPts, '#47ABE8', 1.5); // leicht gedämpft im Hintergrund
plotSamp.drawStems(samples, '#FE756C');
// --- 6. Panel 3: Rekonstruktion ---
const nyquistOk = fs >= 2 * F_SIG;
const reconColor = nyquistOk ? '#3BB583' : '#FE756C';
plotRecon.clear();
// Original als gestrichelte Referenz
const ctx = plotRecon.ctx;
ctx.save();
ctx.setLineDash([5, 5]);
plotRecon.drawLine(analogPts, '#47ABE8', 1.2);
ctx.setLineDash([]);
ctx.restore();
// Rekonstruiertes Signal
plotRecon.drawLine(reconPts, reconColor, 2.2);
// --- 7. UI-Updates ---
updateUI(fs, nyquistOk);
}
function updateUI(fs, ok) {
const infoBox = document.getElementById('infoBox');
const ratio = (fs / F_SIG).toFixed(2);
const fAlias = Math.abs(fs - F_SIG).toFixed(2); // Aliasing-Frequenz (vereinfacht)
if (ok) {
infoBox.style.borderLeftColor = '#1565c0';
infoBox.style.background = '#DAEEFA';
infoBox.innerHTML =
`<strong>✓ Nyquist-Bedingung erfüllt:</strong> ` +
`f<sub>s</sub> = ${fs.toFixed(2)} kHz ≥ 2 · ${F_SIG} kHz = ${(2*F_SIG).toFixed(2)} kHz. ` +
`Das rekonstruierte Signal stimmt mit dem Original überein.`;
} else {
infoBox.style.borderLeftColor = '#c62828';
infoBox.style.background = '#FFE3E2';
infoBox.innerHTML =
`<strong>⚠ Aliasing:</strong> f<sub>s</sub> = ${fs.toFixed(2)} kHz < 2 · ${F_SIG} kHz. ` +
`Das rekonstruierte Signal erscheint mit Aliasing-Frequenz ` +
`f<sub>Alias</sub> = |f<sub>s</sub> − f<sub>Signal</sub>| ≈ ${fAlias} kHz. ` +
`Erhöhe die Abtastrate über ${(2*F_SIG).toFixed(1)} kHz, um das Signal korrekt darzustellen.`;
}
}
// ============================================================
// Event-Listener & Init
// ============================================================
const slider = document.getElementById('sliderFs');
const labelFs = document.getElementById('labelFs');
slider.addEventListener('input', () => {
const fs = parseFloat(slider.value);
labelFs.textContent = fs.toFixed(2) + ' kHz';
render(fs);
});
function onResize() {
if (!plotOrig || !plotSamp || !plotRecon) return;
plotOrig.resize();
plotSamp.resize();
plotRecon.resize();
render(parseFloat(slider.value));
}
window.addEventListener('resize', onResize);
// Erst nach dem Laden initialisieren, wenn Canvas-Dimensionen bekannt sind
window.addEventListener('load', () => {
initPlots();
const fs = parseFloat(slider.value);
labelFs.textContent = fs.toFixed(2) + ' kHz';
render(fs);
});
</script>
</html>