feature: include applet for nyquist

This commit is contained in:
Matthias Jung
2026-02-22 13:40:56 +01:00
parent bc96e4c398
commit 453ee9a5bb
2 changed files with 451 additions and 1 deletions
+447
View File
@@ -0,0 +1,447 @@
<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>