Files
skyview.astronomiemuseum.de/public/blackbody_radiation.php
T

610 lines
20 KiB
PHP
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.
<?php
declare(strict_types=1);
ini_set('display_errors', '1');
error_reporting(E_ALL);
session_start();
$pageTitle = 'Schwarzkörperstrahlung';
$headerIntroPre = 'Astronomie-Werkzeuge';
$headerIntroTitle = 'Schwarzkörperstrahlung';
$headerIntroSub = 'Planck-Kurve für eine frei wählbare Temperatur als interaktives Diagramm.';
require __DIR__ . '/header.php';
?>
<style>
.bb-layout {
display: grid;
grid-template-columns: minmax(18rem, 24rem) minmax(0, 1fr);
gap: 1.5rem;
align-items: start;
}
.bb-panel,
.bb-chart-card {
display: grid;
gap: 1rem;
}
.bb-field-grid {
display: grid;
gap: 1rem;
}
.bb-field {
display: grid;
gap: 0.45rem;
}
.bb-field label {
color: var(--text-dim);
font-size: 0.92rem;
}
.bb-number-row {
display: flex;
align-items: center;
gap: 0.6rem;
}
.bb-number-row input[type="number"] {
width: 100%;
padding: 0.72rem 0.8rem;
border-radius: var(--radius);
border: 1px solid var(--border);
background: rgba(255,255,255,0.04);
color: var(--text);
font-size: 0.96rem;
font-family: inherit;
}
.bb-number-row input[type="number"]:focus,
.bb-field input[type="range"]:focus {
outline: none;
border-color: rgba(201, 168, 76, 0.5);
box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.1);
}
.bb-unit {
color: var(--text-dim);
font-size: 0.88rem;
white-space: nowrap;
}
.bb-field input[type="range"] {
width: 100%;
accent-color: var(--gold);
cursor: pointer;
}
.bb-hint {
color: var(--text-dim);
font-size: 0.8rem;
line-height: 1.5;
}
.bb-presets {
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
}
.bb-toggle {
display: inline-flex;
align-items: center;
gap: 0.55rem;
color: var(--text-dim);
font-size: 0.86rem;
}
.bb-toggle input[type="checkbox"] {
accent-color: var(--gold);
cursor: pointer;
}
.bb-field.is-disabled {
opacity: 0.55;
}
.bb-stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.85rem;
}
.bb-stat {
display: grid;
gap: 0.2rem;
padding: 0.9rem 1rem;
border-radius: var(--radius);
border: 1px solid var(--border);
background: var(--bg-card2);
}
.bb-stat-label {
color: var(--text-dim);
font-size: 0.78rem;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.bb-stat-value {
color: var(--gold-pale);
font-size: 1.18rem;
font-weight: 600;
}
.bb-stat-sub {
color: var(--text-dim);
font-size: 0.8rem;
}
.bb-curve-wrap {
border-radius: var(--radius);
border: 1px solid var(--border);
background:
radial-gradient(circle at top, rgba(201, 168, 76, 0.12), transparent 40%),
linear-gradient(180deg, rgba(10, 14, 28, 0.96), rgba(5, 7, 14, 1));
padding: 0.75rem;
}
#bb-canvas {
display: block;
width: 100%;
height: 28rem;
border-radius: calc(var(--radius) - 0.2rem);
}
.bb-note-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.bb-note {
color: var(--text-dim);
font-size: 0.9rem;
line-height: 1.6;
}
.bb-note strong {
color: var(--text);
}
@media (max-width: 980px) {
.bb-layout {
grid-template-columns: 1fr;
}
.bb-stats,
.bb-note-grid {
grid-template-columns: 1fr;
}
#bb-canvas {
height: 22rem;
}
}
</style>
<div class="bb-layout">
<section class="card bb-panel">
<div>
<h2>Parameter</h2>
<p class="bb-hint">Die Kurve zeigt die spektrale Strahlungsdichte eines idealen Schwarzkörpers nach dem planckschen Strahlungsgesetz.</p>
</div>
<div class="bb-field-grid">
<div class="bb-field">
<label for="bbTemperature">Temperatur</label>
<div class="bb-number-row">
<input id="bbTemperature" type="number" min="100" max="40000" step="10" value="5778">
<span class="bb-unit">K</span>
</div>
<input id="bbTemperatureRange" type="range" min="100" max="40000" step="10" value="5778">
<div class="bb-hint">Typische Bereiche: Glühlampe ~ 2.700 K, Sonne ~ 5.778 K, heißer Stern > 10.000 K.</div>
</div>
<div class="bb-field">
<label for="bbMaxWavelength">Diagrammbereich</label>
<div class="bb-number-row">
<input id="bbMaxWavelength" type="number" min="1000" max="10000" step="100" value="3000">
<span class="bb-unit">nm</span>
</div>
<input id="bbMaxWavelengthRange" type="range" min="1000" max="10000" step="100" value="3000">
<div class="bb-hint">Das Diagramm beginnt bei 100 nm und endet beim hier gewählten Maximalwert.</div>
</div>
<div class="bb-field is-disabled" id="bbYAxisField">
<label for="bbYAxisMax">Y-Achse</label>
<label class="bb-toggle" for="bbYAxisAuto">
<input id="bbYAxisAuto" type="checkbox" checked>
<span>Automatisch skalieren</span>
</label>
<div class="bb-number-row">
<input id="bbYAxisMax" type="number" min="1000000" max="400000000000000000" step="1000000" value="10000000000000" disabled>
<span class="bb-unit">W·sr⁻¹·m⁻³</span>
</div>
<input id="bbYAxisMaxRange" type="range" min="1000000" max="400000000000000000" step="1000000" value="10000000000000" disabled>
<div class="bb-hint">Wenn Auto aus ist, legst du hier den Maximalwert der y-Achse selbst fest.</div>
</div>
</div>
<div>
<div class="bb-hint" style="margin-bottom:0.5rem;">Beispiele</div>
<div class="bb-presets">
<button type="button" class="btn btn-secondary btn-sm" data-temp="300">Erde ~ 300 K</button>
<button type="button" class="btn btn-secondary btn-sm" data-temp="2700">Glühlampe</button>
<button type="button" class="btn btn-secondary btn-sm" data-temp="5778">Sonne</button>
<button type="button" class="btn btn-secondary btn-sm" data-temp="10000">Heißer Stern</button>
</div>
</div>
</section>
<section class="bb-chart-card">
<div class="bb-stats">
<div class="bb-stat">
<span class="bb-stat-label">Wellenlängenmaximum</span>
<span class="bb-stat-value" id="bbPeakNm">–</span>
<span class="bb-stat-sub" id="bbPeakRegion">–</span>
</div>
<div class="bb-stat">
<span class="bb-stat-label">Stefan-Boltzmann</span>
<span class="bb-stat-value" id="bbFlux">–</span>
<span class="bb-stat-sub">Gesamtleistung pro Fläche</span>
</div>
<div class="bb-stat">
<span class="bb-stat-label">Relative Farbwirkung</span>
<span class="bb-stat-value" id="bbColorLabel">–</span>
<span class="bb-stat-sub">grob aus dem Peak-Bereich</span>
</div>
</div>
<div class="card">
<h2>Planck-Kurve</h2>
<div class="bb-curve-wrap">
<canvas id="bb-canvas" aria-label="Diagramm der Schwarzkörperstrahlung"></canvas>
</div>
</div>
<div class="bb-note-grid">
<div class="card bb-note">
<strong>Was zeigt die Kurve?</strong><br>
Auf der x-Achse steht die Wellenlänge in Nanometern, auf der y-Achse die spektrale Strahlungsdichte. Höhere Temperaturen verschieben das Maximum zu kleineren Wellenlängen.
</div>
<div class="card bb-note">
<strong>Wichtige Gesetze</strong><br>
Das Wiensche Verschiebungsgesetz liefert die Lage des Maximums, das Stefan-Boltzmann-Gesetz die gesamte abgestrahlte Leistung über alle Wellenlängen.
</div>
</div>
</section>
</div>
<script>
(function () {
const PLANCK_H = 6.62607015e-34;
const LIGHT_C = 2.99792458e8;
const BOLTZMANN_K = 1.380649e-23;
const STEFAN_BOLTZMANN = 5.670374419e-8;
const WIEN_DISPLACEMENT_NM_K = 2.897771955e6;
const MIN_WAVELENGTH_NM = 100;
const temperatureInput = document.getElementById('bbTemperature');
const temperatureRange = document.getElementById('bbTemperatureRange');
const maxWavelengthInput = document.getElementById('bbMaxWavelength');
const maxWavelengthRange = document.getElementById('bbMaxWavelengthRange');
const yAxisField = document.getElementById('bbYAxisField');
const yAxisAutoInput = document.getElementById('bbYAxisAuto');
const yAxisMaxInput = document.getElementById('bbYAxisMax');
const yAxisMaxRange = document.getElementById('bbYAxisMaxRange');
const peakNmOutput = document.getElementById('bbPeakNm');
const peakRegionOutput = document.getElementById('bbPeakRegion');
const fluxOutput = document.getElementById('bbFlux');
const colorLabelOutput = document.getElementById('bbColorLabel');
const canvas = document.getElementById('bb-canvas');
const ctx = canvas.getContext('2d');
function syncPair(primary, secondary, onChange) {
primary.addEventListener('input', function () {
secondary.value = primary.value;
onChange();
});
secondary.addEventListener('input', function () {
primary.value = secondary.value;
onChange();
});
}
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
function updateYAxisControls() {
const isAuto = yAxisAutoInput.checked;
yAxisMaxInput.disabled = isAuto;
yAxisMaxRange.disabled = isAuto;
yAxisField.classList.toggle('is-disabled', isAuto);
}
function planckSpectralRadiance(lambdaMeters, temperatureKelvin) {
const exponent = (PLANCK_H * LIGHT_C) / (lambdaMeters * BOLTZMANN_K * temperatureKelvin);
if (!Number.isFinite(exponent) || exponent > 700) {
return 0;
}
const numerator = 2 * PLANCK_H * Math.pow(LIGHT_C, 2);
const denominator = Math.pow(lambdaMeters, 5) * (Math.exp(exponent) - 1);
return numerator / denominator;
}
function formatGermanNumber(value, fractionDigits) {
return new Intl.NumberFormat('de-DE', {
minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits
}).format(value);
}
function formatScientific(value) {
if (!Number.isFinite(value) || value <= 0) {
return '0';
}
const exponent = Math.floor(Math.log10(value));
const mantissa = value / Math.pow(10, exponent);
return formatGermanNumber(mantissa, 2) + ' × 10^' + exponent;
}
function describePeakRegion(peakNm) {
if (peakNm < 380) {
return 'Maximum im Ultraviolett';
}
if (peakNm < 450) {
return 'Maximum im violett-blauen Bereich';
}
if (peakNm < 495) {
return 'Maximum im blauen Bereich';
}
if (peakNm < 570) {
return 'Maximum im grün-gelben Bereich';
}
if (peakNm < 590) {
return 'Maximum im gelben Bereich';
}
if (peakNm < 620) {
return 'Maximum im orangefarbenen Bereich';
}
if (peakNm < 750) {
return 'Maximum im roten Bereich';
}
return 'Maximum im Infrarot';
}
function describeColor(peakNm) {
if (peakNm < 380) {
return 'bläulich bis UV-dominiert';
}
if (peakNm < 500) {
return 'bläulich';
}
if (peakNm < 570) {
return 'weißlich';
}
if (peakNm < 620) {
return 'gelblich';
}
if (peakNm < 750) {
return 'orange bis rötlich';
}
return 'rot bis infrarotdominiert';
}
function drawVisibleBand(plotLeft, plotTop, plotWidth, plotHeight, xMin, xMax) {
const visibleStart = Math.max(380, xMin);
const visibleEnd = Math.min(750, xMax);
if (visibleEnd <= visibleStart) {
return;
}
const startX = plotLeft + ((visibleStart - xMin) / (xMax - xMin)) * plotWidth;
const endX = plotLeft + ((visibleEnd - xMin) / (xMax - xMin)) * plotWidth;
const gradient = ctx.createLinearGradient(startX, 0, endX, 0);
gradient.addColorStop(0.00, 'rgba(102, 0, 255, 0.30)');
gradient.addColorStop(0.17, 'rgba(0, 90, 255, 0.30)');
gradient.addColorStop(0.34, 'rgba(0, 200, 255, 0.30)');
gradient.addColorStop(0.50, 'rgba(0, 255, 120, 0.25)');
gradient.addColorStop(0.67, 'rgba(255, 230, 0, 0.25)');
gradient.addColorStop(0.83, 'rgba(255, 140, 0, 0.28)');
gradient.addColorStop(1.00, 'rgba(255, 40, 0, 0.28)');
ctx.fillStyle = gradient;
ctx.fillRect(startX, plotTop, endX - startX, plotHeight);
}
function renderChart() {
const temperature = clamp(Number(temperatureInput.value) || 0, 100, 40000);
const maxWavelengthNm = clamp(Number(maxWavelengthInput.value) || 0, 1000, 10000);
temperatureInput.value = String(temperature);
temperatureRange.value = String(temperature);
maxWavelengthInput.value = String(maxWavelengthNm);
maxWavelengthRange.value = String(maxWavelengthNm);
const peakNm = WIEN_DISPLACEMENT_NM_K / temperature;
const totalFlux = STEFAN_BOLTZMANN * Math.pow(temperature, 4);
peakNmOutput.textContent = formatGermanNumber(peakNm, peakNm < 100 ? 1 : 0) + ' nm';
peakRegionOutput.textContent = describePeakRegion(peakNm);
fluxOutput.textContent = formatScientific(totalFlux) + ' W/m²';
colorLabelOutput.textContent = describeColor(peakNm);
const ratio = Math.min(window.devicePixelRatio || 1, 2);
const width = canvas.clientWidth;
const height = canvas.clientHeight;
canvas.width = Math.round(width * ratio);
canvas.height = Math.round(height * ratio);
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
ctx.clearRect(0, 0, width, height);
const padding = { top: 20, right: 22, bottom: 48, left: 78 };
const plotLeft = padding.left;
const plotTop = padding.top;
const plotWidth = width - padding.left - padding.right;
const plotHeight = height - padding.top - padding.bottom;
const xMin = MIN_WAVELENGTH_NM;
const xMax = maxWavelengthNm;
const samples = Math.max(320, Math.floor(plotWidth));
const points = [];
let dataMaxRadiance = 0;
for (let index = 0; index <= samples; index += 1) {
const wavelengthNm = xMin + ((xMax - xMin) * index / samples);
const radiance = planckSpectralRadiance(wavelengthNm * 1e-9, temperature);
if (radiance > dataMaxRadiance) {
dataMaxRadiance = radiance;
}
points.push({ wavelengthNm, radiance });
}
let maxRadiance = dataMaxRadiance;
if (!yAxisAutoInput.checked) {
const manualMaxRadiance = Number(yAxisMaxInput.value) || 0;
if (Number.isFinite(manualMaxRadiance) && manualMaxRadiance > 0) {
maxRadiance = manualMaxRadiance;
}
}
maxRadiance = Math.max(maxRadiance, dataMaxRadiance * 0.05, 1);
drawVisibleBand(plotLeft, plotTop, plotWidth, plotHeight, xMin, xMax);
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
ctx.lineWidth = 1;
ctx.font = '11px Georgia, serif';
ctx.fillStyle = 'rgba(220, 226, 244, 0.72)';
const xTicks = [100, 250, 500, 750, 1000, 1500, 2000, 3000, 5000, 8000, 10000].filter(function (tick) {
return tick >= xMin && tick <= xMax;
});
xTicks.forEach(function (tick) {
const x = plotLeft + ((tick - xMin) / (xMax - xMin)) * plotWidth;
ctx.beginPath();
ctx.moveTo(x, plotTop);
ctx.lineTo(x, plotTop + plotHeight);
ctx.stroke();
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(String(tick), x, plotTop + plotHeight + 8);
});
const yTicks = 5;
for (let tick = 0; tick <= yTicks; tick += 1) {
const value = maxRadiance * tick / yTicks;
const y = plotTop + plotHeight - (tick / yTicks) * plotHeight;
ctx.beginPath();
ctx.moveTo(plotLeft, y);
ctx.lineTo(plotLeft + plotWidth, y);
ctx.stroke();
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText(formatScientific(value), plotLeft - 10, y);
}
ctx.strokeStyle = 'rgba(255,255,255,0.22)';
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(plotLeft, plotTop);
ctx.lineTo(plotLeft, plotTop + plotHeight);
ctx.lineTo(plotLeft + plotWidth, plotTop + plotHeight);
ctx.stroke();
const areaGradient = ctx.createLinearGradient(0, plotTop, 0, plotTop + plotHeight);
areaGradient.addColorStop(0, 'rgba(201, 168, 76, 0.38)');
areaGradient.addColorStop(1, 'rgba(201, 168, 76, 0.03)');
ctx.beginPath();
points.forEach(function (point, index) {
const x = plotLeft + ((point.wavelengthNm - xMin) / (xMax - xMin)) * plotWidth;
const y = plotTop + plotHeight - ((point.radiance / maxRadiance) * plotHeight);
if (index === 0) {
ctx.moveTo(x, y);
return;
}
ctx.lineTo(x, y);
});
ctx.lineTo(plotLeft + plotWidth, plotTop + plotHeight);
ctx.lineTo(plotLeft, plotTop + plotHeight);
ctx.closePath();
ctx.fillStyle = areaGradient;
ctx.fill();
ctx.beginPath();
points.forEach(function (point, index) {
const x = plotLeft + ((point.wavelengthNm - xMin) / (xMax - xMin)) * plotWidth;
const y = plotTop + plotHeight - ((point.radiance / maxRadiance) * plotHeight);
if (index === 0) {
ctx.moveTo(x, y);
return;
}
ctx.lineTo(x, y);
});
ctx.strokeStyle = '#f0d47b';
ctx.lineWidth = 2.4;
ctx.stroke();
if (peakNm >= xMin && peakNm <= xMax) {
const peakX = plotLeft + ((peakNm - xMin) / (xMax - xMin)) * plotWidth;
ctx.beginPath();
ctx.moveTo(peakX, plotTop);
ctx.lineTo(peakX, plotTop + plotHeight);
ctx.strokeStyle = 'rgba(79, 195, 216, 0.95)';
ctx.lineWidth = 1.4;
ctx.setLineDash([6, 5]);
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#4fc3d8';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText('λmax', Math.min(peakX + 6, plotLeft + plotWidth - 42), plotTop + 6);
}
ctx.fillStyle = 'rgba(240, 244, 255, 0.85)';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillText('Wellenlänge in nm', plotLeft + plotWidth / 2, height - 10);
ctx.save();
ctx.translate(18, plotTop + plotHeight / 2);
ctx.rotate(-Math.PI / 2);
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText('Spektrale Strahlungsdichte Bλ', 0, 0);
ctx.restore();
}
syncPair(temperatureInput, temperatureRange, renderChart);
syncPair(maxWavelengthInput, maxWavelengthRange, renderChart);
syncPair(yAxisMaxInput, yAxisMaxRange, renderChart);
yAxisAutoInput.addEventListener('change', function () {
updateYAxisControls();
renderChart();
});
document.querySelectorAll('[data-temp]').forEach(function (button) {
button.addEventListener('click', function () {
const value = button.getAttribute('data-temp') || '5778';
temperatureInput.value = value;
temperatureRange.value = value;
renderChart();
});
});
window.addEventListener('resize', renderChart);
updateYAxisControls();
renderChart();
})();
</script>
<?php require __DIR__ . '/footer.php'; ?>