|
|
@@ -1,7 +1,1476 @@
|
|
|
-<tr valign="top" textalign="left">
|
|
|
-<td> 12 </td><td> Сцена 4 · модуляция </td><td> Число циклов огибающей зашито константой, ползунок F_м меняет только спектр, но не осциллограмму. Не обоснована необходимость модуляции </td><td> Окно времени фиксировано (4 мс), число циклов = F_м × T. Добавить график модулирующего сигнала. Добавить текст: антенна эффективна при λ/4, для звука 1 кГц — 75 км, для несущей 1 МГц — 75 м </td><td> 3.1.500/001_s4_3 </td>
|
|
|
-</tr><tr valign="top" textalign="left">
|
|
|
-<td colspan=5>
|
|
|
-<b>Что изменилось в сцене 4:</b> <code>const cycles = 4</code> заменено на <code>nCycles = Fm * T_WINDOW_MS</code> при <code>T_WINDOW_MS = 4</code>. Число циклов модулирующего сигнала и огибающей в окне теперь 4 при F_м=1 кГц и 80 при F_м=20 кГц. Число отсчётов адаптивное: <code>max(300, ceil(nCycles * 20))</code> — при 80 циклах 1600 отсчётов, шаг штриховки несущей 8, чтобы не рисовать 1600 линий. Добавлен верхний график «Модулирующий сигнал (звук)» — синус с частотой F_м, зелёный. Средний график «АМ-сигнал» — штриховка несущей (постоянная плотность) между огибающими (красные). Нижний — спектр. Перед canvas — блок с обоснованием: антенна эффективна при длине ~ λ/4, звук 1 кГц требует 75 км, несущая 1 МГц — 75 м. Canvas вырос с 260 до 420 px. Обратная связь дополнена пояснением про F_м: низкая — редкая огибающая, узкая полоса; высокая — плотная, широкая, нужна более высокая несущая.
|
|
|
-</td>
|
|
|
-</tr>
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="ru">
|
|
|
+<head>
|
|
|
+<meta charset="UTF-8">
|
|
|
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+<title>Эфир-1: Принципы радиосвязи</title>
|
|
|
+<style>
|
|
|
+:root {
|
|
|
+ --paper: #f4ecd8;
|
|
|
+ --paper-dark: #e8dcc0;
|
|
|
+ --ink: #1a2a4a;
|
|
|
+ --ink-light: #3a4a6a;
|
|
|
+ --red: #b22222;
|
|
|
+ --green: #2a6a3a;
|
|
|
+ --blue: #1a4a7a;
|
|
|
+ --mono: 'Roboto Mono', 'Courier New', monospace;
|
|
|
+ --serif: 'PT Serif', Georgia, serif;
|
|
|
+ --sans: 'Oswald', 'Arial Narrow', sans-serif;
|
|
|
+}
|
|
|
+* { box-sizing: border-box; }
|
|
|
+body {
|
|
|
+ margin: 0;
|
|
|
+ background: var(--paper);
|
|
|
+ color: var(--ink);
|
|
|
+ font-family: var(--serif);
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+header {
|
|
|
+ background: var(--ink);
|
|
|
+ color: var(--paper);
|
|
|
+ padding: 12px 20px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+header h1 {
|
|
|
+ font-family: var(--sans);
|
|
|
+ font-size: 22px;
|
|
|
+ margin: 0;
|
|
|
+ letter-spacing: 2px;
|
|
|
+ text-transform: uppercase;
|
|
|
+}
|
|
|
+header .sub {
|
|
|
+ font-family: var(--mono);
|
|
|
+ font-size: 12px;
|
|
|
+ opacity: 0.7;
|
|
|
+}
|
|
|
+#progress-bar { height: 4px; background: var(--paper-dark); }
|
|
|
+#progress-fill { height: 100%; background: var(--red); width: 11.1%; transition: width 0.3s; }
|
|
|
+main { max-width: 900px; margin: 0 auto; padding: 20px; }
|
|
|
+.scene { display: none; }
|
|
|
+.scene.active { display: block; animation: fadeIn 0.25s; }
|
|
|
+@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
|
|
+h2 {
|
|
|
+ font-family: var(--sans);
|
|
|
+ color: var(--red);
|
|
|
+ border-bottom: 2px solid var(--red);
|
|
|
+ padding-bottom: 6px;
|
|
|
+ letter-spacing: 1px;
|
|
|
+ text-transform: uppercase;
|
|
|
+ font-size: 22px;
|
|
|
+}
|
|
|
+p { margin: 8px 0; }
|
|
|
+.lead { font-size: 17px; color: var(--ink-light); }
|
|
|
+ul { padding-left: 22px; }
|
|
|
+canvas { display: block; width: 100%; background: var(--paper-dark); border: 1px solid var(--ink); margin: 12px 0; }
|
|
|
+.controls { background: var(--paper-dark); border: 1px solid var(--ink); padding: 12px 16px; margin: 12px 0; }
|
|
|
+.control-row { display: flex; align-items: center; gap: 12px; margin: 8px 0; flex-wrap: wrap; }
|
|
|
+.control-row label { font-family: var(--mono); font-size: 13px; min-width: 240px; }
|
|
|
+input[type=range] { flex: 1; min-width: 140px; accent-color: var(--red); }
|
|
|
+.value-box {
|
|
|
+ font-family: var(--mono);
|
|
|
+ background: var(--ink);
|
|
|
+ color: var(--paper);
|
|
|
+ padding: 2px 8px;
|
|
|
+ font-size: 13px;
|
|
|
+ min-width: 90px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.readout {
|
|
|
+ font-family: var(--mono);
|
|
|
+ font-size: 14px;
|
|
|
+ background: var(--ink);
|
|
|
+ color: var(--paper);
|
|
|
+ padding: 10px 14px;
|
|
|
+ margin: 12px 0;
|
|
|
+ line-height: 1.7;
|
|
|
+}
|
|
|
+.readout .hl { color: #ffd700; }
|
|
|
+.feedback {
|
|
|
+ border-left: 4px solid var(--red);
|
|
|
+ background: rgba(178,34,34,0.08);
|
|
|
+ padding: 10px 14px;
|
|
|
+ margin: 12px 0;
|
|
|
+ font-size: 15px;
|
|
|
+}
|
|
|
+.notebook {
|
|
|
+ background: #fffef5;
|
|
|
+ border: 2px solid var(--ink);
|
|
|
+ border-left: 6px solid var(--red);
|
|
|
+ padding: 16px 20px;
|
|
|
+ margin: 16px 0;
|
|
|
+}
|
|
|
+.notebook .nb-title {
|
|
|
+ font-family: var(--sans);
|
|
|
+ color: var(--red);
|
|
|
+ font-size: 16px;
|
|
|
+ letter-spacing: 2px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ text-transform: uppercase;
|
|
|
+}
|
|
|
+.notebook dt { font-family: var(--mono); font-weight: bold; margin-top: 8px; }
|
|
|
+.notebook dd { margin: 0; color: var(--ink-light); font-family: var(--mono); font-size: 13px; }
|
|
|
+.btn {
|
|
|
+ font-family: var(--sans);
|
|
|
+ background: var(--ink);
|
|
|
+ color: var(--paper);
|
|
|
+ border: none;
|
|
|
+ padding: 10px 24px;
|
|
|
+ font-size: 15px;
|
|
|
+ letter-spacing: 1px;
|
|
|
+ cursor: pointer;
|
|
|
+ text-transform: uppercase;
|
|
|
+ transition: background 0.2s;
|
|
|
+}
|
|
|
+.btn:hover { background: var(--red); }
|
|
|
+.btn.small { padding: 6px 16px; font-size: 13px; }
|
|
|
+.btn.active { background: var(--red); }
|
|
|
+.nav { display: flex; justify-content: space-between; margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--ink); }
|
|
|
+.legend { font-family: var(--mono); font-size: 12px; color: var(--ink-light); margin: 8px 0; }
|
|
|
+.cards-area { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; }
|
|
|
+.card {
|
|
|
+ background: var(--paper-dark);
|
|
|
+ border: 1px solid var(--ink);
|
|
|
+ padding: 8px 12px;
|
|
|
+ font-family: var(--mono);
|
|
|
+ font-size: 13px;
|
|
|
+ cursor: pointer;
|
|
|
+ user-select: none;
|
|
|
+ transition: all 0.15s;
|
|
|
+}
|
|
|
+.card:hover { background: #fff; }
|
|
|
+.card.selected { border-color: var(--red); border-width: 2px; background: #fff; box-shadow: 0 0 0 2px rgba(178,34,34,0.25); }
|
|
|
+.bins-area { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; margin: 12px 0; }
|
|
|
+.bin {
|
|
|
+ border: 2px dashed var(--ink);
|
|
|
+ min-height: 90px;
|
|
|
+ padding: 6px;
|
|
|
+ text-align: center;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.15s;
|
|
|
+}
|
|
|
+.bin:hover { border-color: var(--red); background: rgba(178,34,34,0.05); }
|
|
|
+.bin .bin-title { font-family: var(--sans); font-size: 12px; letter-spacing: 1px; color: var(--red); display: block; margin-bottom: 4px; }
|
|
|
+.bin .bin-item { font-family: var(--mono); font-size: 10px; display: block; padding: 1px 0; color: var(--green); }
|
|
|
+.question { background: var(--paper-dark); border: 1px solid var(--ink); padding: 14px 18px; margin: 14px 0; }
|
|
|
+.question .q-num { font-family: var(--sans); color: var(--red); font-size: 14px; letter-spacing: 1px; }
|
|
|
+.question .q-text { font-size: 16px; margin: 4px 0 10px; }
|
|
|
+.question .options label { display: block; padding: 6px 10px; margin: 4px 0; cursor: pointer; font-size: 15px; border: 1px solid transparent; }
|
|
|
+.question .options label:hover { background: rgba(0,0,0,0.04); }
|
|
|
+.question .options input { margin-right: 8px; }
|
|
|
+.question .explain { display: none; margin-top: 10px; padding: 8px 12px; font-size: 14px; border-left: 3px solid var(--red); background: rgba(178,34,34,0.08); }
|
|
|
+.question .explain.show { display: block; }
|
|
|
+.question .explain.correct { border-color: var(--green); background: rgba(42,106,58,0.1); }
|
|
|
+.toggles { display: flex; gap: 12px; flex-wrap: wrap; margin: 12px 0; }
|
|
|
+.toggle-item {
|
|
|
+ background: var(--paper-dark);
|
|
|
+ border: 1px solid var(--ink);
|
|
|
+ padding: 10px 14px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-family: var(--mono);
|
|
|
+ font-size: 13px;
|
|
|
+ user-select: none;
|
|
|
+ transition: all 0.15s;
|
|
|
+}
|
|
|
+.toggle-item.on { background: var(--green); color: var(--paper); border-color: var(--green); }
|
|
|
+.risk-bars { margin: 12px 0; }
|
|
|
+.risk-row { display: flex; align-items: center; gap: 10px; margin: 6px 0; font-family: var(--mono); font-size: 13px; }
|
|
|
+.risk-row .risk-label { min-width: 110px; }
|
|
|
+.risk-track { flex: 1; height: 16px; background: var(--paper-dark); border: 1px solid var(--ink); }
|
|
|
+.risk-fill { height: 100%; transition: width 0.3s, background 0.3s; }
|
|
|
+.checklist { background: #fffef5; border: 2px solid var(--green); padding: 16px 20px; margin: 20px 0; }
|
|
|
+.checklist .nb-title { font-family: var(--sans); color: var(--green); font-size: 16px; letter-spacing: 2px; margin-bottom: 8px; }
|
|
|
+.checklist ul { margin: 0; padding-left: 20px; }
|
|
|
+.checklist li { margin: 4px 0; font-family: var(--mono); font-size: 13px; }
|
|
|
+.score-box { font-family: var(--sans); font-size: 24px; color: var(--red); text-align: center; padding: 16px; border: 2px solid var(--red); margin: 20px 0; }
|
|
|
+footer { text-align: center; padding: 20px; font-family: var(--mono); font-size: 11px; color: var(--ink-light); border-top: 1px solid var(--ink); margin-top: 40px; }
|
|
|
+@media (max-width: 600px) {
|
|
|
+ .bins-area { grid-template-columns: repeat(2, 1fr); }
|
|
|
+ .control-row label { min-width: 140px; font-size: 12px; }
|
|
|
+ header h1 { font-size: 16px; }
|
|
|
+ main { padding: 12px; }
|
|
|
+}
|
|
|
+</style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+
|
|
|
+<header>
|
|
|
+ <h1>Эфир-1 · Принципы радиосвязи</h1>
|
|
|
+ <div class="sub">ПМ.03.01 · Заслон · v0.1-spec</div>
|
|
|
+</header>
|
|
|
+<div id="progress-bar"><div id="progress-fill"></div></div>
|
|
|
+
|
|
|
+<main id="app">
|
|
|
+
|
|
|
+<!-- 1 -->
|
|
|
+<section class="scene active" data-scene="1">
|
|
|
+ <h2>1. Цель</h2>
|
|
|
+ <p class="lead">Этот интерактив — про то, как радиоволна выбирается из передатчика, что с ней происходит по дороге и почему разные диапазоны ведут себя по-разному.</p>
|
|
|
+ <p>После прохождения вы сможете:</p>
|
|
|
+ <ul>
|
|
|
+ <li>по частоте определить диапазон, длину волны и место на шкале ЭМ излучений;</li>
|
|
|
+ <li>объяснить, почему ДВ огибает холм, а Wi-Fi за тем же холмом не работает;</li>
|
|
|
+ <li>объяснить, почему КВ слышно за тысячи километров, а УКВ — только в прямой видимости;</li>
|
|
|
+ <li>выбрать диапазон под задачу (подводная связь, вещание, мобильная, спутник);</li>
|
|
|
+ <li>объяснить, как направленная антенна, ППРЧ и экранирование снижают риск перехвата и подавления.</li>
|
|
|
+ </ul>
|
|
|
+ <div class="nav"><span></span><button class="btn" onclick="goTo(2)">Начать →</button></div>
|
|
|
+</section>
|
|
|
+
|
|
|
+<!-- 2 -->
|
|
|
+<section class="scene" data-scene="2">
|
|
|
+ <h2>2. Актуализация: волна и шкала</h2>
|
|
|
+ <p>Колеблющийся заряд создаёт переменное электромагнитное поле, которое распространяется в пространстве со скоростью света. Частота f и длина волны λ связаны: <b>λ = c / f</b>, c = 3·10⁸ м/с.</p>
|
|
|
+ <p>Двигайте ползунок — частота, длина волны и энергия кванта меняются вместе.</p>
|
|
|
+ <canvas id="c-spectrum"></canvas>
|
|
|
+ <p class="legend">Видимый свет — узкая полоска в середине шкалы. Глаз видит только её.</p>
|
|
|
+ <div class="controls">
|
|
|
+ <div class="control-row">
|
|
|
+ <label>Частота f</label>
|
|
|
+ <input type="range" id="s-freq" min="3.48" max="20.48" step="0.01" value="6">
|
|
|
+ <span class="value-box" id="v-freq">1 МГц</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="readout" id="r-spectrum"></div>
|
|
|
+ <div class="feedback" id="fb-spectrum"></div>
|
|
|
+ <div class="nav"><button class="btn" onclick="goTo(1)">← Назад</button><button class="btn" onclick="goTo(3)">Далее →</button></div>
|
|
|
+</section>
|
|
|
+
|
|
|
+<!-- 3 -->
|
|
|
+<section class="scene" data-scene="3">
|
|
|
+ <h2>3. Определения для тетради</h2>
|
|
|
+ <div class="notebook">
|
|
|
+ <div class="nb-title">ЗАПИШИ В ТЕТРАДЬ</div>
|
|
|
+ <dl>
|
|
|
+ <dt>Электромагнитная волна</dt>
|
|
|
+ <dd>Распространяющееся в пространстве переменное ЭМ поле. В вакууме c = 3·10⁸ м/с.</dd>
|
|
|
+ <dt>Длина волны</dt>
|
|
|
+ <dd>λ = c / f, [м]. Частота f, [Гц].</dd>
|
|
|
+ <dt>Модулирующий сигнал</dt>
|
|
|
+ <dd>Низкочастотный сигнал, содержащий информацию (звук, данные). Обозначение F<sub>м</sub>, [Гц].</dd>
|
|
|
+ <dt>Несущая</dt>
|
|
|
+ <dd>Высокочастотное колебание, переносящее сигнал. Обозначение f<sub>c</sub>, [Гц].</dd>
|
|
|
+ <dt>Модуляция</dt>
|
|
|
+ <dd>Изменение параметров несущей по закону модулирующего сигнала.</dd>
|
|
|
+ <dt>Полоса пропускания</dt>
|
|
|
+ <dd>Интервал частот, занимаемый сигналом. Для АМ: B = 2F<sub>м</sub>.</dd>
|
|
|
+ <dt>Дифракция</dt>
|
|
|
+ <dd>Огибание волной препятствий, соизмеримых с λ.</dd>
|
|
|
+ <dt>Ионосфера</dt>
|
|
|
+ <dd>Ионизированные слои атмосферы (D, E, F), отражающие КВ.</dd>
|
|
|
+ <dt>Радиогоризонт</dt>
|
|
|
+ <dd>d ≈ 3.57·(√h₁ + √h₂) км, h₁,h₂ — высоты антенн в метрах.</dd>
|
|
|
+ </dl>
|
|
|
+ </div>
|
|
|
+ <p class="legend">Переход не блокируется: не записали — вернитесь позже. Определения короткие, в тетрадь, не для чтения.</p>
|
|
|
+ <div class="nav"><button class="btn" onclick="goTo(2)">← Назад</button><button class="btn" onclick="goTo(4)">Далее →</button></div>
|
|
|
+</section>
|
|
|
+
|
|
|
+<!-- 4 -->
|
|
|
+<section class="scene" data-scene="4">
|
|
|
+ <h2>4. Принцип радиосвязи: несущая и модуляция</h2>
|
|
|
+ <p>Информацию переносит не сама несущая, а её изменение. Передатчик формирует высокочастотную несущую, модулятор накладывает на неё сигнал, антенна излучает. Приёмник отделяет сигнал обратно.</p>
|
|
|
+
|
|
|
+ <div class="feedback" style="border-left-color:#2a6a3a;background:rgba(42,106,58,0.08)">
|
|
|
+ <b>Зачем вообще модуляция?</b> Антенна эффективна, когда её длина соизмерима с λ/4. Для звука 1 кГц λ = 300 км, антенна — 75 км. Нереально. Для несущей 1 МГц λ = 300 м, антенна — 75 м. Уже можно. Модуляция сдвигает сигнал на высокую частоту, где антенна работает. Плюс: на разных несущих можно передавать разные сигналы одновременно — частотное разделение каналов.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <p><b>Модулирующий сигнал F<sub>м</sub></b> — низкочастотный, содержит информацию. <b>Несущая f<sub>c</sub></b> — высокочастотная, переносит её. При амплитудной модуляции амплитуда несущей меняется по закону модулирующего сигнала. Значит <b>огибающая АМ-сигнала повторяет F<sub>м</sub></b>, а заполнение под ней — это несущая f<sub>c</sub>. Глубина модуляции m — насколько сильно, полоса B = 2F<sub>м</sub>.</p>
|
|
|
+ <canvas id="c-mod"></canvas>
|
|
|
+ <div class="controls">
|
|
|
+ <div class="control-row">
|
|
|
+ <label>Модулирующий сигнал (звук) F<sub>м</sub></label>
|
|
|
+ <input type="range" id="s-fm" min="1" max="20" step="0.5" value="5">
|
|
|
+ <span class="value-box" id="v-fm">5 кГц</span>
|
|
|
+ </div>
|
|
|
+ <div class="control-row">
|
|
|
+ <label>Несущая f<sub>c</sub></label>
|
|
|
+ <input type="range" id="s-fc" min="0.5" max="5" step="0.1" value="1">
|
|
|
+ <span class="value-box" id="v-fc">1.0 МГц</span>
|
|
|
+ </div>
|
|
|
+ <div class="control-row">
|
|
|
+ <label>Глубина модуляции m</label>
|
|
|
+ <input type="range" id="s-m" min="0" max="1.5" step="0.01" value="0.7">
|
|
|
+ <span class="value-box" id="v-m">0.70</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="readout" id="r-mod"></div>
|
|
|
+ <div class="feedback" id="fb-mod"></div>
|
|
|
+ <p class="legend">Верхний график — модулирующий сигнал F_м (низкочастотный, зелёный). Средний — несущая f_c (синяя), <b>то же число осцилляций</b>, что на нижнем. Нижний — АМ-сигнал: <b style="color:#b22222">красная огибающая точно повторяет верхний график (F_м)</b>, <b style="color:#1a4a7a">заполнение под ней — несущая f_c, то же число осцилляций, что на среднем</b>. При движении F_м меняется только огибающая (верхний и нижний синхронно). При движении f_c — только число осцилляций несущей (средний и нижний синхронно). Масштаб осцилляций условный (реальное отношение f_c/F_м = 200:1 нечитаемо), реальные λ — в readout.</p>
|
|
|
+ <div class="nav"><button class="btn" onclick="goTo(3)">← Назад</button><button class="btn" onclick="goTo(5)">Далее →</button></div>
|
|
|
+</section>
|
|
|
+
|
|
|
+<!-- 5 -->
|
|
|
+<section class="scene" data-scene="5">
|
|
|
+ <h2>5. Распространение: дифракция и прямая видимость</h2>
|
|
|
+ <p>Волна огибает препятствие, если её длина соизмерима с препятствием. Если λ много меньше — за препятствием тень.</p>
|
|
|
+ <p>Геометрия: передатчик и приёмник на земле, по 10 км от препятствия. Препятствие — ножевой край на 200 м выше линии прямой видимости. Меняйте λ и смотрите, что доходит до приёмника.</p>
|
|
|
+ <canvas id="c-diff"></canvas>
|
|
|
+ <div class="controls">
|
|
|
+ <div class="control-row">
|
|
|
+ <label>Длина волны λ</label>
|
|
|
+ <input type="range" id="s-lam" min="-1" max="5" step="0.01" value="1">
|
|
|
+ <span class="value-box" id="v-lam">10 м</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="readout" id="r-diff"></div>
|
|
|
+ <div class="feedback" id="fb-diff"></div>
|
|
|
+ <p class="legend">Вертикальный масштаб увеличен. Модель ножевого края (ITU-R P.526); реальный холм даёт меньше потерь, но качественно то же.</p>
|
|
|
+ <div class="nav"><button class="btn" onclick="goTo(4)">← Назад</button><button class="btn" onclick="goTo(6)">Далее →</button></div>
|
|
|
+</section>
|
|
|
+
|
|
|
+<!-- 6 -->
|
|
|
+<section class="scene" data-scene="6">
|
|
|
+ <h2>6. Ионосфера: отражение и поглощение</h2>
|
|
|
+ <p>Почему ДВ/СВ/КВ идут за горизонт, а УКВ — нет. Меняйте частоту и время суток.</p>
|
|
|
+ <canvas id="c-iono"></canvas>
|
|
|
+ <div class="controls">
|
|
|
+ <div class="control-row">
|
|
|
+ <label>Частота f</label>
|
|
|
+ <input type="range" id="s-iono-f" min="5" max="10" step="0.01" value="6.5">
|
|
|
+ <span class="value-box" id="v-iono-f">3 МГц</span>
|
|
|
+ </div>
|
|
|
+ <div class="control-row">
|
|
|
+ <label>Время суток</label>
|
|
|
+ <button class="btn small active" id="b-day">День</button>
|
|
|
+ <button class="btn small" id="b-night">Ночь</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="readout" id="r-iono"></div>
|
|
|
+ <div class="feedback" id="fb-iono"></div>
|
|
|
+ <p class="legend">Модель пороговая: 3 МГц и 30 МГц — качественные границы, не формула. Для инженерного расчёта — МПЧ, закон секанса, солнечная активность.</p>
|
|
|
+ <div class="nav"><button class="btn" onclick="goTo(5)">← Назад</button><button class="btn" onclick="goTo(7)">Далее →</button></div>
|
|
|
+</section>
|
|
|
+
|
|
|
+<!-- 7 -->
|
|
|
+<section class="scene" data-scene="7">
|
|
|
+ <h2>7. Тренажёр диапазонов</h2>
|
|
|
+ <p>Выберите карточку, затем нажмите на корзину. Верно — карточка встанет на место с пояснением. Неверно — подсказка, попробуйте снова.</p>
|
|
|
+ <div id="trainer"></div>
|
|
|
+ <div class="feedback" id="fb-trainer" style="display:none"></div>
|
|
|
+ <div class="nav"><button class="btn" onclick="goTo(6)">← Назад</button><button class="btn" onclick="goTo(8)">Далее →</button></div>
|
|
|
+</section>
|
|
|
+
|
|
|
+<!-- 8 -->
|
|
|
+<section class="scene" data-scene="8">
|
|
|
+ <h2>8. Применение и ИБ: атака и защита</h2>
|
|
|
+ <p>Распространение волн — это ещё и зона перехвата. Чем шире зона, тем больше возможностей у «красных».</p>
|
|
|
+ <canvas id="c-sec"></canvas>
|
|
|
+ <div class="toggles" id="sec-toggles">
|
|
|
+ <div class="toggle-item" data-key="antenna">Направленная антенна</div>
|
|
|
+ <div class="toggle-item" data-key="fhss">ППРЧ (перестройка частоты)</div>
|
|
|
+ <div class="toggle-item" data-key="shield">Экранирование</div>
|
|
|
+ </div>
|
|
|
+ <div class="risk-bars" id="risk-bars"></div>
|
|
|
+ <div class="feedback" id="fb-sec"></div>
|
|
|
+ <p class="legend">Связь с триадой КЦД: перехват — конфиденциальность, подавление — доступность, имитация — целостность.</p>
|
|
|
+ <div class="nav"><button class="btn" onclick="goTo(7)">← Назад</button><button class="btn" onclick="goTo(9)">Далее →</button></div>
|
|
|
+</section>
|
|
|
+
|
|
|
+<!-- 9 -->
|
|
|
+<section class="scene" data-scene="9">
|
|
|
+ <h2>9. Проверка</h2>
|
|
|
+ <p>Семь вопросов. После ответа — пояснение. Оценка — за первую попытку.</p>
|
|
|
+ <div id="test-container"></div>
|
|
|
+ <div class="score-box" id="score-box">Отвечено: 0 / 7</div>
|
|
|
+ <div style="text-align:center"><button class="btn" onclick="initTest()">Пересдать</button></div>
|
|
|
+ <div class="checklist" id="checklist" style="display:none">
|
|
|
+ <div class="nb-title">ЧЕК-ЛИСТ КОНСПЕКТА</div>
|
|
|
+ <ul>
|
|
|
+ <li>λ = c / f, c = 3·10⁸ м/с</li>
|
|
|
+ <li>B = 2F<sub>м</sub> — полоса АМ-сигнала</li>
|
|
|
+ <li>Огибающая АМ повторяет модулирующий сигнал F_м; заполнение = несущая f_c</li>
|
|
|
+ <li>Дифракция: λ соизмерима с препятствием → огибает</li>
|
|
|
+ <li>Ионосфера отражает КВ, прозрачна для УКВ</li>
|
|
|
+ <li>Радиогоризонт: d ≈ 3.57·(√h₁ + √h₂) км</li>
|
|
|
+ <li>Направленная антенна сужает зону перехвата, ППРЧ защищает от узкополосной помехи, экранирование снижает утечку</li>
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+ <div class="nav"><button class="btn" onclick="goTo(8)">← Назад</button><span></span></div>
|
|
|
+</section>
|
|
|
+
|
|
|
+</main>
|
|
|
+
|
|
|
+<footer>Эфир-1 · Заслон · ПМ.03.01 · Модель не прибор · v0.1-spec</footer>
|
|
|
+
|
|
|
+<script>
|
|
|
+'use strict';
|
|
|
+
|
|
|
+let currentScene = 1;
|
|
|
+let currentDraw = null;
|
|
|
+
|
|
|
+/* ---------- helpers ---------- */
|
|
|
+function prepCanvas(canvas, cssH) {
|
|
|
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
|
+ const W = canvas.clientWidth || 800;
|
|
|
+ const H = cssH;
|
|
|
+ if (canvas.width !== Math.round(W * dpr) || canvas.height !== Math.round(H * dpr)) {
|
|
|
+ canvas.width = Math.round(W * dpr);
|
|
|
+ canvas.height = Math.round(H * dpr);
|
|
|
+ canvas.style.height = H + 'px';
|
|
|
+ }
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
|
+ ctx.clearRect(0, 0, W, H);
|
|
|
+ return { ctx, W, H };
|
|
|
+}
|
|
|
+
|
|
|
+function formatFreq(f) {
|
|
|
+ if (f >= 1e18) return (f / 1e18).toFixed(1) + ' ЭГц';
|
|
|
+ if (f >= 1e15) return (f / 1e15).toFixed(1) + ' ПГц';
|
|
|
+ if (f >= 1e12) return (f / 1e12).toFixed(1) + ' ТГц';
|
|
|
+ if (f >= 1e9) return (f / 1e9).toFixed(1) + ' ГГц';
|
|
|
+ if (f >= 1e6) return (f / 1e6).toFixed(1) + ' МГц';
|
|
|
+ if (f >= 1e3) return (f / 1e3).toFixed(1) + ' кГц';
|
|
|
+ return f.toFixed(0) + ' Гц';
|
|
|
+}
|
|
|
+
|
|
|
+function formatLambda(l) {
|
|
|
+ if (l >= 1000) return (l / 1000).toFixed(1) + ' км';
|
|
|
+ if (l >= 1) return l.toFixed(1) + ' м';
|
|
|
+ if (l >= 0.01) return (l * 100).toFixed(1) + ' см';
|
|
|
+ if (l >= 0.001) return (l * 1000).toFixed(1) + ' мм';
|
|
|
+ if (l >= 1e-6) return (l * 1e6).toFixed(1) + ' мкм';
|
|
|
+ if (l >= 1e-9) return (l * 1e9).toFixed(1) + ' нм';
|
|
|
+ return (l * 1e12).toFixed(1) + ' пм';
|
|
|
+}
|
|
|
+
|
|
|
+function formatEV(eV) {
|
|
|
+ if (eV >= 1e6) return (eV / 1e6).toFixed(1) + ' МэВ';
|
|
|
+ if (eV >= 1e3) return (eV / 1e3).toFixed(1) + ' кэВ';
|
|
|
+ if (eV >= 1) return eV.toFixed(2) + ' эВ';
|
|
|
+ if (eV >= 1e-3) return (eV * 1e3).toFixed(2) + ' мэВ';
|
|
|
+ return (eV * 1e6).toFixed(2) + ' мкэВ';
|
|
|
+}
|
|
|
+
|
|
|
+/* ---------- navigation ---------- */
|
|
|
+function goTo(n) {
|
|
|
+ currentDraw = null;
|
|
|
+ document.querySelectorAll('.scene').forEach(s => s.classList.remove('active'));
|
|
|
+ const scene = document.querySelector('.scene[data-scene="' + n + '"]');
|
|
|
+ if (scene) scene.classList.add('active');
|
|
|
+ currentScene = n;
|
|
|
+ document.getElementById('progress-fill').style.width = (n / 9 * 100) + '%';
|
|
|
+ window.scrollTo(0, 0);
|
|
|
+ if (n === 2) initSpectrum();
|
|
|
+ if (n === 4) initMod();
|
|
|
+ if (n === 5) initDiff();
|
|
|
+ if (n === 6) initIono();
|
|
|
+ if (n === 7) initTrainer();
|
|
|
+ if (n === 8) initSecurity();
|
|
|
+ if (n === 9) initTest();
|
|
|
+}
|
|
|
+
|
|
|
+window.addEventListener('resize', () => { if (currentDraw) currentDraw(); });
|
|
|
+
|
|
|
+/* ============================================================
|
|
|
+ SCENE 2 — EM spectrum
|
|
|
+ ============================================================ */
|
|
|
+function initSpectrum() {
|
|
|
+ const canvas = document.getElementById('c-spectrum');
|
|
|
+ const slider = document.getElementById('s-freq');
|
|
|
+ const logMin = 3.48, logMax = 20.48;
|
|
|
+
|
|
|
+ const bands = [
|
|
|
+ { from: 3.48, to: 4.48, color: '#3a5a85', label: 'СДВ' },
|
|
|
+ { from: 4.48, to: 5.48, color: '#4a6fa5', label: 'ДВ' },
|
|
|
+ { from: 5.48, to: 6.48, color: '#5a7fb5', label: 'СВ' },
|
|
|
+ { from: 6.48, to: 7.48, color: '#6a8fc5', label: 'КВ' },
|
|
|
+ { from: 7.48, to: 9.48, color: '#7a9fd5', label: 'УКВ' },
|
|
|
+ { from: 9.48, to: 11.48, color: '#8aafe5', label: 'СВЧ' },
|
|
|
+ { from: 11.48, to: 14.63, color: '#d08050', label: 'ИК' },
|
|
|
+ { from: 14.63, to: 14.9, color: '#e0d040', label: '' },
|
|
|
+ { from: 14.9, to: 16.48, color: '#a060c0', label: 'УФ' },
|
|
|
+ { from: 16.48, to: 19.48, color: '#7040a0', label: 'Рентг.' },
|
|
|
+ { from: 19.48, to: 20.48, color: '#402060', label: 'Гамма' }
|
|
|
+ ];
|
|
|
+
|
|
|
+ function bandInfo(logf) {
|
|
|
+ if (logf < 11.48) return { name: 'Радиоволны', comment: 'Энергия кванта мала — вещество не ионизирует. Проникает сквозь диэлектрики, отражается от металла.' };
|
|
|
+ if (logf < 14.63) return { name: 'Инфракрасное', comment: 'Тепловое излучение. Греет, но не ионизирует.' };
|
|
|
+ if (logf < 14.9) return { name: 'Видимое', comment: 'Узкая полоска — глаз видит только её. Энергия кванта ~2 эВ.' };
|
|
|
+ if (logf < 16.48) return { name: 'Ультрафиолет', comment: 'Начинает ионизировать. Загар, витамин D, но и ожоги.' };
|
|
|
+ if (logf < 19.48) return { name: 'Рентген', comment: 'Ионизирует сильно. Проникает сквозь мягкие ткани.' };
|
|
|
+ return { name: 'Гамма', comment: 'Ионизирует очень сильно. Требует тяжёлой защиты.' };
|
|
|
+ }
|
|
|
+
|
|
|
+ function draw() {
|
|
|
+ const { ctx, W, H } = prepCanvas(canvas, 180);
|
|
|
+ const logf = parseFloat(slider.value);
|
|
|
+ const f = Math.pow(10, logf);
|
|
|
+
|
|
|
+ const margin = 30;
|
|
|
+ const barY = 55, barH = 60;
|
|
|
+ const x0 = margin, x1 = W - margin;
|
|
|
+ const range = logMax - logMin;
|
|
|
+ const xOf = lf => x0 + (lf - logMin) / range * (x1 - x0);
|
|
|
+
|
|
|
+ ctx.fillStyle = '#1a2a4a';
|
|
|
+ ctx.font = '11px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'left';
|
|
|
+ ctx.fillText('ШКАЛА ЭЛЕКТРОМАГНИТНЫХ ИЗЛУЧЕНИЙ (лог. масштаб)', x0, 20);
|
|
|
+
|
|
|
+ bands.forEach(b => {
|
|
|
+ const bx0 = xOf(b.from), bx1 = xOf(b.to);
|
|
|
+ ctx.fillStyle = b.color;
|
|
|
+ ctx.fillRect(bx0, barY, bx1 - bx0, barH);
|
|
|
+ if (b.label && bx1 - bx0 > 26) {
|
|
|
+ ctx.fillStyle = '#fff';
|
|
|
+ ctx.font = '10px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.fillText(b.label, (bx0 + bx1) / 2, barY + barH / 2 + 3);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ ctx.strokeStyle = '#1a2a4a';
|
|
|
+ ctx.lineWidth = 1;
|
|
|
+ ctx.strokeRect(x0, barY, x1 - x0, barH);
|
|
|
+
|
|
|
+ const mx = xOf(logf);
|
|
|
+ ctx.fillStyle = '#b22222';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(mx, barY - 4);
|
|
|
+ ctx.lineTo(mx - 7, barY - 16);
|
|
|
+ ctx.lineTo(mx + 7, barY - 16);
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fill();
|
|
|
+ ctx.fillRect(mx - 1, barY, 2, barH);
|
|
|
+
|
|
|
+ ctx.fillStyle = '#1a2a4a';
|
|
|
+ ctx.font = '9px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ [3.48, 6.48, 9.48, 12.48, 15.48, 18.48, 20.48].forEach(lf => {
|
|
|
+ ctx.fillText(formatFreq(Math.pow(10, lf)), xOf(lf), barY + barH + 14);
|
|
|
+ });
|
|
|
+
|
|
|
+ const lambda = 3e8 / f;
|
|
|
+ const eV = 4.136e-15 * f;
|
|
|
+ document.getElementById('v-freq').textContent = formatFreq(f);
|
|
|
+ document.getElementById('r-spectrum').innerHTML =
|
|
|
+ 'Частота: <span class="hl">' + formatFreq(f) + '</span><br>' +
|
|
|
+ 'Длина волны: <span class="hl">' + formatLambda(lambda) + '</span><br>' +
|
|
|
+ 'Энергия кванта: <span class="hl">' + formatEV(eV) + '</span>';
|
|
|
+ const info = bandInfo(logf);
|
|
|
+ document.getElementById('fb-spectrum').innerHTML = '<b>' + info.name + '.</b> ' + info.comment;
|
|
|
+ }
|
|
|
+
|
|
|
+ currentDraw = draw;
|
|
|
+ slider.oninput = draw;
|
|
|
+ draw();
|
|
|
+}
|
|
|
+
|
|
|
+/* ============================================================
|
|
|
+ SCENE 4 — modulation
|
|
|
+
|
|
|
+ ЛОГИКА:
|
|
|
+ - Модулирующий F_м (низкочастотный) → огибающая АМ.
|
|
|
+ - Несущая f_c (высокочастотная) → заполнение под огибающей.
|
|
|
+ - Число осцилляций несущей ОДНО И ТО ЖЕ на среднем и нижнем графиках.
|
|
|
+ - Зависит ТОЛЬКО от f_c. Масштаб условный (реальное 200:1 нечитаемо).
|
|
|
+ ============================================================ */
|
|
|
+function initMod() {
|
|
|
+ const canvas = document.getElementById('c-mod');
|
|
|
+ const sFm = document.getElementById('s-fm');
|
|
|
+ const sFc = document.getElementById('s-fc');
|
|
|
+ const sM = document.getElementById('s-m');
|
|
|
+
|
|
|
+ const T_MOD_MS = 4; // окно времени для модулирующего и АМ, мс
|
|
|
+
|
|
|
+ function draw() {
|
|
|
+ const { ctx, W, H } = prepCanvas(canvas, 540);
|
|
|
+ const Fm = parseFloat(sFm.value); // кГц
|
|
|
+ const Fc = parseFloat(sFc.value); // МГц
|
|
|
+ const m = parseFloat(sM.value);
|
|
|
+
|
|
|
+ // Число циклов модулирующего в окне — только от Fm
|
|
|
+ const nMod = Fm * T_MOD_MS;
|
|
|
+
|
|
|
+ // Число осцилляций несущей в окне — только от Fc, ОДНО И ТО ЖЕ на обоих графиках.
|
|
|
+ // 30..200 — условный масштаб, чтобы было читаемо при любом f_c.
|
|
|
+ // Реальная f_c за 4 мс дала бы 2000–20000 периодов — нечитаемо.
|
|
|
+ const nOscDisplay = 30 + 170 * (Fc - 0.5) / 4.5;
|
|
|
+
|
|
|
+ const x0 = 55, x1 = W - 20;
|
|
|
+ const midX = (x0 + x1) / 2;
|
|
|
+
|
|
|
+ // ============================================================
|
|
|
+ // ГРАФИК 1 (верхний): МОДУЛИРУЮЩИЙ СИГНАЛ F_м — низкочастотный
|
|
|
+ // ============================================================
|
|
|
+ const mY = 22, mH = 80;
|
|
|
+ const mMid = mY + mH / 2;
|
|
|
+ ctx.fillStyle = '#1a2a4a';
|
|
|
+ ctx.font = '10px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'left';
|
|
|
+ ctx.fillText('МОДУЛИРУЮЩИЙ СИГНАЛ F_м = ' + Fm.toFixed(1) + ' кГц — низкочастотный, содержит информацию', x0, mY - 6);
|
|
|
+
|
|
|
+ ctx.strokeStyle = '#1a2a4a';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x0, mMid);
|
|
|
+ ctx.lineTo(x1, mMid);
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x0, mY);
|
|
|
+ ctx.lineTo(x0, mY + mH);
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ const mSamples = Math.max(200, Math.ceil(nMod * 20));
|
|
|
+ ctx.strokeStyle = '#2a6a3a';
|
|
|
+ ctx.lineWidth = 2;
|
|
|
+ ctx.beginPath();
|
|
|
+ for (let i = 0; i <= mSamples; i++) {
|
|
|
+ const x = x0 + (x1 - x0) * i / mSamples;
|
|
|
+ const phase = (i / mSamples) * nMod * 2 * Math.PI;
|
|
|
+ const y = mMid - (mH / 2 - 6) * Math.sin(phase);
|
|
|
+ if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
|
+ }
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.lineWidth = 1;
|
|
|
+
|
|
|
+ ctx.fillStyle = '#3a4a6a';
|
|
|
+ ctx.font = '9px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'right';
|
|
|
+ ctx.fillText('циклов: ' + nMod.toFixed(0), x1, mY - 6);
|
|
|
+
|
|
|
+ // ============================================================
|
|
|
+ // ГРАФИК 2 (средний): НЕСУЩАЯ f_c — высокочастотная
|
|
|
+ // ТО ЖЕ число осцилляций nOscDisplay, что на нижнем
|
|
|
+ // ============================================================
|
|
|
+ const cY = 145, cH = 80;
|
|
|
+ const cMid = cY + cH / 2;
|
|
|
+ ctx.fillStyle = '#1a2a4a';
|
|
|
+ ctx.font = '10px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'left';
|
|
|
+ ctx.fillText('НЕСУЩАЯ f_c = ' + Fc.toFixed(1) + ' МГц — высокочастотная, переносит сигнал', x0, cY - 6);
|
|
|
+
|
|
|
+ ctx.strokeStyle = '#1a2a4a';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x0, cMid);
|
|
|
+ ctx.lineTo(x1, cMid);
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x0, cY);
|
|
|
+ ctx.lineTo(x0, cY + cH);
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ const cSamples = Math.max(600, Math.ceil(nOscDisplay * 16));
|
|
|
+ ctx.strokeStyle = '#1a4a7a';
|
|
|
+ ctx.lineWidth = 1.5;
|
|
|
+ ctx.beginPath();
|
|
|
+ for (let i = 0; i <= cSamples; i++) {
|
|
|
+ const x = x0 + (x1 - x0) * i / cSamples;
|
|
|
+ const phase = (i / cSamples) * nOscDisplay * 2 * Math.PI;
|
|
|
+ const y = cMid - (cH / 2 - 4) * Math.sin(phase);
|
|
|
+ if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
|
+ }
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.lineWidth = 1;
|
|
|
+
|
|
|
+ ctx.fillStyle = '#3a4a6a';
|
|
|
+ ctx.font = '9px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'right';
|
|
|
+ ctx.fillText('осцилляций: ' + nOscDisplay.toFixed(0), x1, cY - 6);
|
|
|
+
|
|
|
+ // ============================================================
|
|
|
+ // ГРАФИК 3 (нижний): АМ-СИГНАЛ
|
|
|
+ // огибающая = 1 + m·sin(2π·nMod·t) → совпадает с ВЕРХНИМ (F_м)
|
|
|
+ // заполнение = sin(2π·nOscDisplay·t) → совпадает со СРЕДНИМ (f_c)
|
|
|
+ // ============================================================
|
|
|
+ const tY = 265, tH = 140;
|
|
|
+ const tMid = tY + tH / 2;
|
|
|
+ ctx.fillStyle = '#1a2a4a';
|
|
|
+ ctx.font = '10px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'left';
|
|
|
+ ctx.fillText('АМ-СИГНАЛ: огибающая = модулирующий F_м, заполнение = несущая f_c', x0, tY - 6);
|
|
|
+
|
|
|
+ ctx.strokeStyle = '#1a2a4a';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x0, tMid);
|
|
|
+ ctx.lineTo(x1, tMid);
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x0, tY);
|
|
|
+ ctx.lineTo(x0, tY + tH);
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ const ampBase = (tH / 2 - 6) / 1.6;
|
|
|
+
|
|
|
+ // Красные огибающие ±(1 + m·sin(2π·nMod·t))
|
|
|
+ ctx.strokeStyle = 'rgba(178,34,34,0.9)';
|
|
|
+ ctx.lineWidth = 2;
|
|
|
+ for (let sign = -1; sign <= 1; sign += 2) {
|
|
|
+ ctx.beginPath();
|
|
|
+ for (let i = 0; i <= 400; i++) {
|
|
|
+ const x = x0 + (x1 - x0) * i / 400;
|
|
|
+ const tNorm = i / 400;
|
|
|
+ const env = 1 + m * Math.sin(2 * Math.PI * nMod * tNorm);
|
|
|
+ const y = tMid - sign * ampBase * env;
|
|
|
+ if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
|
+ }
|
|
|
+ ctx.stroke();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Сам АМ-сигнал: то же nOscDisplay, что на среднем
|
|
|
+ const tSamples = Math.max(1200, Math.ceil(nOscDisplay * 16));
|
|
|
+ ctx.strokeStyle = '#1a4a7a';
|
|
|
+ ctx.lineWidth = 1.5;
|
|
|
+ ctx.beginPath();
|
|
|
+ for (let i = 0; i <= tSamples; i++) {
|
|
|
+ const x = x0 + (x1 - x0) * i / tSamples;
|
|
|
+ const tNorm = i / tSamples;
|
|
|
+ const env = 1 + m * Math.sin(2 * Math.PI * nMod * tNorm);
|
|
|
+ const osc = Math.sin(2 * Math.PI * nOscDisplay * tNorm);
|
|
|
+ const y = tMid - ampBase * env * osc;
|
|
|
+ if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
|
+ }
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.lineWidth = 1;
|
|
|
+
|
|
|
+ // Подпись под графиком
|
|
|
+ ctx.font = '9px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'left';
|
|
|
+ if (m > 1) {
|
|
|
+ ctx.fillStyle = '#b22222';
|
|
|
+ ctx.font = 'bold 11px "Roboto Mono", monospace';
|
|
|
+ ctx.fillText('ПЕРЕМОДУЛЯЦИЯ: огибающая пересекает ноль', x0, tY + tH + 14);
|
|
|
+ ctx.fillStyle = '#3a4a6a';
|
|
|
+ ctx.font = '9px "Roboto Mono", monospace';
|
|
|
+ ctx.fillText('огибающая = верхний (' + nMod.toFixed(0) + ' циклов); заполнение = средний (' + nOscDisplay.toFixed(0) + ' осц.) — синхронно', x0, tY + tH + 28);
|
|
|
+ } else {
|
|
|
+ ctx.fillStyle = '#3a4a6a';
|
|
|
+ ctx.fillText('огибающая = верхний (' + nMod.toFixed(0) + ' циклов); заполнение = средний (' + nOscDisplay.toFixed(0) + ' осц.) — синхронно', x0, tY + tH + 14);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ============================================================
|
|
|
+ // ГРАФИК 4: Спектр
|
|
|
+ // ============================================================
|
|
|
+ const sY = 440, sH = 80;
|
|
|
+ const sBase = sY + sH;
|
|
|
+ ctx.fillStyle = '#1a2a4a';
|
|
|
+ ctx.font = '10px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'left';
|
|
|
+ ctx.fillText('СПЕКТР: несущая f_c и две боковые на f_c ± F_м', x0, sY - 6);
|
|
|
+
|
|
|
+ ctx.strokeStyle = '#1a2a4a';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x0, sBase);
|
|
|
+ ctx.lineTo(x1, sBase);
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ const fcX = midX;
|
|
|
+ const maxDev = 30;
|
|
|
+ const pxPerKHz = (x1 - x0) * 0.35 / maxDev;
|
|
|
+ const sbOff = pxPerKHz * Fm;
|
|
|
+
|
|
|
+ function drawStick(x, h, color, label) {
|
|
|
+ ctx.strokeStyle = color;
|
|
|
+ ctx.lineWidth = 2;
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x, sBase);
|
|
|
+ ctx.lineTo(x, sBase - h);
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.fillStyle = color;
|
|
|
+ ctx.font = '10px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.fillText(label, x, sBase - h - 5);
|
|
|
+ }
|
|
|
+
|
|
|
+ drawStick(fcX, 50, '#1a4a7a', 'f_c');
|
|
|
+ if (m > 0) {
|
|
|
+ drawStick(fcX - sbOff, 50 * m / 2, '#b22222', 'f_c−F_м');
|
|
|
+ drawStick(fcX + sbOff, 50 * m / 2, '#b22222', 'f_c+F_м');
|
|
|
+
|
|
|
+ ctx.strokeStyle = '#2a6a3a';
|
|
|
+ ctx.setLineDash([3, 3]);
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(fcX - sbOff, sBase - 60);
|
|
|
+ ctx.lineTo(fcX + sbOff, sBase - 60);
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.setLineDash([]);
|
|
|
+ ctx.fillStyle = '#2a6a3a';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.fillText('B = 2F_м = ' + (2 * Fm).toFixed(1) + ' кГц', fcX, sBase - 66);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ============================================================
|
|
|
+ // Показания и обратная связь
|
|
|
+ // ============================================================
|
|
|
+ const lamC = 300 / Fc;
|
|
|
+ const lamMod = 3e8 / (Fm * 1e3);
|
|
|
+ const lamModStr = lamMod >= 1000 ? (lamMod / 1000).toFixed(1) + ' км' : lamMod.toFixed(0) + ' м';
|
|
|
+ const lamModQuarter = lamMod / 4 >= 1000 ? (lamMod / 4000).toFixed(1) + ' км' : (lamMod / 4).toFixed(0) + ' м';
|
|
|
+
|
|
|
+ document.getElementById('v-fm').textContent = Fm.toFixed(1) + ' кГц';
|
|
|
+ document.getElementById('v-fc').textContent = Fc.toFixed(1) + ' МГц';
|
|
|
+ document.getElementById('v-m').textContent = m.toFixed(2);
|
|
|
+
|
|
|
+ document.getElementById('r-mod').innerHTML =
|
|
|
+ 'F_м = <span class="hl">' + Fm.toFixed(1) + ' кГц</span> (модулирующий сигнал, λ = ' + lamModStr + ', λ/4 = ' + lamModQuarter + ')<br>' +
|
|
|
+ 'f_c = <span class="hl">' + Fc.toFixed(1) + ' МГц</span> (несущая, λ = ' + lamC.toFixed(0) + ' м, λ/4 = ' + (lamC / 4).toFixed(0) + ' м)<br>' +
|
|
|
+ 'm = <span class="hl">' + m.toFixed(2) + '</span> · B = 2F_м = <span class="hl">' + (2 * Fm).toFixed(1) + ' кГц</span>';
|
|
|
+
|
|
|
+ let fb;
|
|
|
+ if (m === 0) fb = 'Несущая есть, информации нет. Глубина модуляции нулевая.';
|
|
|
+ else if (m <= 0.5) fb = 'Малая глубина — сигнал слабый, но искажений нет.';
|
|
|
+ else if (m <= 1) fb = 'Глубина в норме. Боковые растут, полоса B = 2F_м.';
|
|
|
+ else fb = 'Перемодуляция: огибающая пересекает ноль. Приёмник искажает.';
|
|
|
+
|
|
|
+ if (Fm <= 3) fb += ' Низкая F_м: огибающая редкая, полоса узкая, но информации мало.';
|
|
|
+ else if (Fm >= 15) fb += ' Высокая F_м: огибающая плотная, полоса широкая. Нужна более высокая несущая, чтобы боковые не налезли на ноль.';
|
|
|
+
|
|
|
+ if (Fc * 1e6 < 10 * Fm * 1e3) {
|
|
|
+ fb += ' Внимание: f_c меньше 10·F_м — боковые налезают друг на друга, огибающая на графике не читается. Это не ошибка модели, это физика.';
|
|
|
+ }
|
|
|
+
|
|
|
+ document.getElementById('fb-mod').textContent = fb;
|
|
|
+ }
|
|
|
+
|
|
|
+ currentDraw = draw;
|
|
|
+ sFm.oninput = draw;
|
|
|
+ sFc.oninput = draw;
|
|
|
+ sM.oninput = draw;
|
|
|
+ draw();
|
|
|
+}
|
|
|
+
|
|
|
+/* ============================================================
|
|
|
+ SCENE 5 — diffraction
|
|
|
+ ============================================================ */
|
|
|
+function initDiff() {
|
|
|
+ const canvas = document.getElementById('c-diff');
|
|
|
+ const slider = document.getElementById('s-lam');
|
|
|
+
|
|
|
+ function draw() {
|
|
|
+ const { ctx, W, H } = prepCanvas(canvas, 280);
|
|
|
+ const logLam = parseFloat(slider.value);
|
|
|
+ const lambda = Math.pow(10, logLam);
|
|
|
+
|
|
|
+ const v = 4 / Math.sqrt(lambda);
|
|
|
+ let L = 0;
|
|
|
+ if (v > -0.7) {
|
|
|
+ L = 6.9 + 20 * Math.log10(Math.sqrt((v - 0.1) * (v - 0.1) + 1) + v - 0.1);
|
|
|
+ }
|
|
|
+ if (!isFinite(L) || L < 0) L = 0;
|
|
|
+
|
|
|
+ const groundY = H - 20;
|
|
|
+ const losY = groundY - 25;
|
|
|
+ const txX = 50, rxX = W - 50;
|
|
|
+ const obsX = W / 2;
|
|
|
+ const obsTopY = losY - 150;
|
|
|
+
|
|
|
+ ctx.fillStyle = '#c8b890';
|
|
|
+ ctx.fillRect(0, groundY, W, H - groundY);
|
|
|
+ ctx.strokeStyle = '#1a2a4a';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(0, groundY);
|
|
|
+ ctx.lineTo(W, groundY);
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ ctx.strokeStyle = 'rgba(26,42,74,0.3)';
|
|
|
+ ctx.setLineDash([4, 4]);
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(txX, losY);
|
|
|
+ ctx.lineTo(rxX, losY);
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.setLineDash([]);
|
|
|
+
|
|
|
+ const shadowAlpha = Math.max(0.03, Math.min(0.6, 0.6 * (1 - L / 40)));
|
|
|
+
|
|
|
+ const maxR = Math.hypot(rxX - txX, groundY - losY) + 40;
|
|
|
+ const steps = 44;
|
|
|
+ for (let r = 40; r < maxR; r += 28) {
|
|
|
+ for (let s = 0; s < steps; s++) {
|
|
|
+ const th1 = (s / steps) * Math.PI;
|
|
|
+ const th2 = ((s + 1) / steps) * Math.PI;
|
|
|
+ const x1 = txX + r * Math.cos(th1);
|
|
|
+ const y1 = losY - r * Math.sin(th1);
|
|
|
+ const x2 = txX + r * Math.cos(th2);
|
|
|
+ const y2 = losY - r * Math.sin(th2);
|
|
|
+ if (y1 > groundY || y2 > groundY) continue;
|
|
|
+ if (x1 < 2 || x1 > W - 2 || x2 < 2 || x2 > W - 2) continue;
|
|
|
+
|
|
|
+ let alpha = 0.5;
|
|
|
+ const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
|
|
|
+ if (mx > obsX) {
|
|
|
+ const yShadow = losY - 150 * (mx - txX) / (obsX - txX);
|
|
|
+ if (my > yShadow) alpha = shadowAlpha;
|
|
|
+ }
|
|
|
+ ctx.strokeStyle = 'rgba(26,42,74,' + alpha + ')';
|
|
|
+ ctx.lineWidth = 1;
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x1, y1);
|
|
|
+ ctx.lineTo(x2, y2);
|
|
|
+ ctx.stroke();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.fillStyle = '#8a7a5a';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(obsX - 7, groundY);
|
|
|
+ ctx.lineTo(obsX, obsTopY);
|
|
|
+ ctx.lineTo(obsX + 7, groundY);
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fill();
|
|
|
+ ctx.strokeStyle = '#1a2a4a';
|
|
|
+ ctx.lineWidth = 1;
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ ctx.fillStyle = '#1a2a4a';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(txX, losY - 15);
|
|
|
+ ctx.lineTo(txX - 8, losY + 5);
|
|
|
+ ctx.lineTo(txX + 8, losY + 5);
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fill();
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(rxX, losY - 15);
|
|
|
+ ctx.lineTo(rxX - 8, losY + 5);
|
|
|
+ ctx.lineTo(rxX + 8, losY + 5);
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fill();
|
|
|
+ ctx.font = '10px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.fillText('ПРД', txX, losY + 20);
|
|
|
+ ctx.fillText('ПРМ', rxX, losY + 20);
|
|
|
+
|
|
|
+ const barH = 44, barW = 12;
|
|
|
+ const barX = rxX + 18;
|
|
|
+ const level = Math.max(0, Math.min(1, 1 - L / 40));
|
|
|
+ ctx.fillStyle = '#e8dcc0';
|
|
|
+ ctx.fillRect(barX, losY - barH / 2, barW, barH);
|
|
|
+ ctx.fillStyle = L < 10 ? '#2a6a3a' : L < 25 ? '#d4a020' : '#b22222';
|
|
|
+ const fillH = barH * level;
|
|
|
+ ctx.fillRect(barX, losY - barH / 2 + (barH - fillH), barW, fillH);
|
|
|
+ ctx.strokeStyle = '#1a2a4a';
|
|
|
+ ctx.strokeRect(barX, losY - barH / 2, barW, barH);
|
|
|
+
|
|
|
+ ctx.fillStyle = '#1a2a4a';
|
|
|
+ ctx.textAlign = 'left';
|
|
|
+ ctx.font = '10px "Roboto Mono", monospace';
|
|
|
+ ctx.fillText('λ = ' + formatLambda(lambda), 10, 18);
|
|
|
+
|
|
|
+ document.getElementById('v-lam').textContent = formatLambda(lambda);
|
|
|
+ document.getElementById('r-diff').innerHTML =
|
|
|
+ 'λ = <span class="hl">' + formatLambda(lambda) + '</span><br>' +
|
|
|
+ 'Параметр Френеля v = <span class="hl">' + v.toFixed(2) + '</span><br>' +
|
|
|
+ 'Потери L = <span class="hl">' + L.toFixed(1) + ' дБ</span>';
|
|
|
+
|
|
|
+ let fb;
|
|
|
+ if (L < 8) fb = 'λ соизмерима с препятствием. Дифракция: волна огибает, сигнал доходит.';
|
|
|
+ else if (L < 18) fb = 'Частичная тень: волна огибает край, но с потерями.';
|
|
|
+ else fb = 'λ много меньше препятствия. Тень: за препятствием сигнала нет.';
|
|
|
+ document.getElementById('fb-diff').textContent = fb;
|
|
|
+ }
|
|
|
+
|
|
|
+ currentDraw = draw;
|
|
|
+ slider.oninput = draw;
|
|
|
+ draw();
|
|
|
+}
|
|
|
+
|
|
|
+/* ============================================================
|
|
|
+ SCENE 6 — ionosphere
|
|
|
+ ============================================================ */
|
|
|
+function initIono() {
|
|
|
+ const canvas = document.getElementById('c-iono');
|
|
|
+ const slider = document.getElementById('s-iono-f');
|
|
|
+ const bDay = document.getElementById('b-day');
|
|
|
+ const bNight = document.getElementById('b-night');
|
|
|
+ let isDay = true;
|
|
|
+
|
|
|
+ function updateDayBtn() {
|
|
|
+ bDay.classList.toggle('active', isDay);
|
|
|
+ bNight.classList.toggle('active', !isDay);
|
|
|
+ }
|
|
|
+
|
|
|
+ function draw() {
|
|
|
+ const { ctx, W, H } = prepCanvas(canvas, 300);
|
|
|
+ const logf = parseFloat(slider.value);
|
|
|
+ const f = Math.pow(10, logf);
|
|
|
+
|
|
|
+ const groundY = H - 20;
|
|
|
+ const topY = 20;
|
|
|
+ const altMax = 400;
|
|
|
+ const yOf = km => groundY - (km / altMax) * (groundY - topY);
|
|
|
+
|
|
|
+ ctx.fillStyle = '#c8b890';
|
|
|
+ ctx.fillRect(0, groundY, W, H - groundY);
|
|
|
+ ctx.strokeStyle = '#1a2a4a';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(0, groundY);
|
|
|
+ ctx.lineTo(W, groundY);
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ const layers = [
|
|
|
+ { name: 'D', from: 60, to: 90, color: 'rgba(100,100,140,0.30)', show: isDay },
|
|
|
+ { name: 'E', from: 90, to: 120, color: 'rgba(100,140,100,0.30)', show: true },
|
|
|
+ { name: 'F', from: 150, to: 400, color: 'rgba(140,100,100,0.20)', show: true }
|
|
|
+ ];
|
|
|
+ layers.forEach(l => {
|
|
|
+ if (!l.show) return;
|
|
|
+ const y1 = yOf(l.from), y2 = yOf(l.to);
|
|
|
+ ctx.fillStyle = l.color;
|
|
|
+ ctx.fillRect(0, y2, W, y1 - y2);
|
|
|
+ ctx.strokeStyle = 'rgba(26,42,74,0.4)';
|
|
|
+ ctx.setLineDash([4, 4]);
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(0, y2);
|
|
|
+ ctx.lineTo(W, y2);
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.setLineDash([]);
|
|
|
+ ctx.fillStyle = '#1a2a4a';
|
|
|
+ ctx.font = '10px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'left';
|
|
|
+ ctx.fillText(l.name + ' (' + l.from + '–' + l.to + ' км)', 6, (y1 + y2) / 2 + 3);
|
|
|
+ });
|
|
|
+
|
|
|
+ const txX = 80, rxX = W - 80;
|
|
|
+
|
|
|
+ const gwAlpha = f > 3e7 ? 0.3 : 0.85;
|
|
|
+ ctx.strokeStyle = 'rgba(42,106,58,' + gwAlpha + ')';
|
|
|
+ ctx.lineWidth = 2;
|
|
|
+ ctx.beginPath();
|
|
|
+ for (let x = txX; x <= rxX; x += 3) {
|
|
|
+ const t = (x - txX) / (rxX - txX);
|
|
|
+ const amp = 4 * (1 - t);
|
|
|
+ const y = groundY - 2 - amp * Math.sin((x - txX) * 0.15);
|
|
|
+ if (x === txX) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
|
+ }
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ let reflectKm = 0, absorb = false;
|
|
|
+ if (f < 3e6) { reflectKm = 110; absorb = isDay; }
|
|
|
+ else if (f <= 3e7) { reflectKm = 300; }
|
|
|
+ else { reflectKm = 0; }
|
|
|
+
|
|
|
+ if (reflectKm > 0) {
|
|
|
+ const midX = (txX + rxX) / 2;
|
|
|
+ const peakY = yOf(reflectKm);
|
|
|
+ ctx.strokeStyle = absorb ? 'rgba(178,34,34,0.55)' : '#1a2a4a';
|
|
|
+ ctx.lineWidth = 2;
|
|
|
+ ctx.setLineDash(absorb ? [6, 4] : []);
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(txX, groundY);
|
|
|
+ ctx.lineTo(midX, peakY);
|
|
|
+ ctx.lineTo(rxX, groundY);
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.setLineDash([]);
|
|
|
+ ctx.fillStyle = '#1a2a4a';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.arc(midX, peakY, 3, 0, Math.PI * 2);
|
|
|
+ ctx.fill();
|
|
|
+ ctx.font = '10px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.fillText('отражение от ' + (reflectKm > 200 ? 'F' : 'E'), midX, peakY - 8);
|
|
|
+ } else {
|
|
|
+ ctx.strokeStyle = '#1a2a4a';
|
|
|
+ ctx.lineWidth = 2;
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(txX, groundY);
|
|
|
+ ctx.lineTo(txX + 220, topY);
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.fillStyle = '#1a2a4a';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(txX + 220, topY);
|
|
|
+ ctx.lineTo(txX + 210, topY + 12);
|
|
|
+ ctx.lineTo(txX + 230, topY + 12);
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fill();
|
|
|
+ ctx.textAlign = 'left';
|
|
|
+ ctx.fillText('в космос', txX + 236, topY + 6);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isDay && f < 3e6) {
|
|
|
+ const y1 = yOf(60), y2 = yOf(90);
|
|
|
+ ctx.fillStyle = 'rgba(178,34,34,0.32)';
|
|
|
+ ctx.fillRect(0, y2, W, y1 - y2);
|
|
|
+ ctx.fillStyle = '#b22222';
|
|
|
+ ctx.font = 'bold 11px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.fillText('ПОГЛОЩЕНИЕ В D-СЛОЕ', W / 2, yOf(75) + 4);
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.fillStyle = '#1a2a4a';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(txX, groundY - 20);
|
|
|
+ ctx.lineTo(txX - 6, groundY);
|
|
|
+ ctx.lineTo(txX + 6, groundY);
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fill();
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(rxX, groundY - 20);
|
|
|
+ ctx.lineTo(rxX - 6, groundY);
|
|
|
+ ctx.lineTo(rxX + 6, groundY);
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fill();
|
|
|
+ ctx.font = '10px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.fillText('ПРД', txX, groundY + 14);
|
|
|
+ ctx.fillText('ПРМ', rxX, groundY + 14);
|
|
|
+
|
|
|
+ document.getElementById('v-iono-f').textContent = formatFreq(f);
|
|
|
+ let band, desc;
|
|
|
+ if (f < 3e6) {
|
|
|
+ band = 'ДВ/СВ';
|
|
|
+ desc = isDay
|
|
|
+ ? 'День: D-слой поглощает, дальний приём пропадает. Земная волна вдоль поверхности.'
|
|
|
+ : 'Ночь: D-слой исчез, отражение от E. Дальняя связь.';
|
|
|
+ } else if (f <= 3e7) {
|
|
|
+ band = 'КВ';
|
|
|
+ desc = 'Отражение от F-слоя, многократные скачки. Мёртвая зона.';
|
|
|
+ } else {
|
|
|
+ band = 'УКВ и выше';
|
|
|
+ desc = 'Проходит сквозь ионосферу. Только прямая видимость.';
|
|
|
+ }
|
|
|
+ document.getElementById('r-iono').innerHTML =
|
|
|
+ 'f = <span class="hl">' + formatFreq(f) + '</span> · ' + band + '<br>' + desc;
|
|
|
+ }
|
|
|
+
|
|
|
+ bDay.onclick = () => { isDay = true; updateDayBtn(); draw(); };
|
|
|
+ bNight.onclick = () => { isDay = false; updateDayBtn(); draw(); };
|
|
|
+ slider.oninput = draw;
|
|
|
+ updateDayBtn();
|
|
|
+ currentDraw = draw;
|
|
|
+ draw();
|
|
|
+}
|
|
|
+
|
|
|
+/* ============================================================
|
|
|
+ SCENE 7 — range trainer
|
|
|
+ ============================================================ */
|
|
|
+const rangeData = [
|
|
|
+ { id: 1, text: 'Связь с подводной лодкой', bin: 'dv', expl: 'ДВ/СДВ проникают в воду на десятки метров (скин-слой). УКВ в воде затухает за сантиметры.' },
|
|
|
+ { id: 2, text: 'Международное КВ-вещание', bin: 'kv', expl: 'КВ отражается от ионосферы (слой F) и огибает Землю многократными скачками.' },
|
|
|
+ { id: 3, text: 'Местная AM-радиостанция', bin: 'sv', expl: 'СВ — сотни кГц. Земная волна вдоль поверхности. Днём D-слой поглощает дальний приём.' },
|
|
|
+ { id: 4, text: 'Wi-Fi 2.4 ГГц', bin: 'svch', expl: '2.4 ГГц — СВЧ. Прямая видимость в помещении, мебель огибает плохо.' },
|
|
|
+ { id: 5, text: 'Мобильный телефон 900 МГц', bin: 'ukv', expl: '900 МГц — УКВ (дециметровый). Прямая видимость до базовой станции, дифракция за зданиями слабая.' },
|
|
|
+ { id: 6, text: 'Радиорелейная линия', bin: 'svch', expl: 'СВЧ, узкий луч, прямая видимость. Между вышками — потому что за горизонт не уходит.' },
|
|
|
+ { id: 7, text: 'Спутниковое ТВ', bin: 'svch', expl: 'СВЧ проходит сквозь ионосферу — спутник видит Землю, Земля видит спутник.' },
|
|
|
+ { id: 8, text: 'Радиолокатор аэропорта', bin: 'svch', expl: 'СВЧ, узкий луч, малая λ — высокое разрешение по дальности и углу.' },
|
|
|
+ { id: 9, text: 'Телевидение ДМВ', bin: 'ukv', expl: 'ДМВ (дециметровые) — УКВ. Прямая видимость, вышки на холмах.' },
|
|
|
+ { id: 10, text: 'Навигационный радиомаяк', bin: 'dv', expl: 'ДВ/СВ — земная волна, устойчивый приём на большие расстояния.' }
|
|
|
+];
|
|
|
+const binNames = { dv: 'ДВ/СДВ', sv: 'СВ', kv: 'КВ', ukv: 'УКВ', svch: 'СВЧ' };
|
|
|
+
|
|
|
+function initTrainer() {
|
|
|
+ const container = document.getElementById('trainer');
|
|
|
+ const fb = document.getElementById('fb-trainer');
|
|
|
+ let selectedCard = null;
|
|
|
+ let placed = {};
|
|
|
+
|
|
|
+ function showFeedback(text, correct) {
|
|
|
+ if (correct) {
|
|
|
+ fb.textContent = 'Верно. ' + text;
|
|
|
+ fb.style.borderColor = '#2a6a3a';
|
|
|
+ fb.style.background = 'rgba(42,106,58,0.1)';
|
|
|
+ } else {
|
|
|
+ fb.textContent = 'Неверно. Вспомните: как волна распространяется в этом диапазоне? Где он на шкале?';
|
|
|
+ fb.style.borderColor = '#b22222';
|
|
|
+ fb.style.background = 'rgba(178,34,34,0.1)';
|
|
|
+ }
|
|
|
+ fb.style.display = 'block';
|
|
|
+ }
|
|
|
+
|
|
|
+ function render() {
|
|
|
+ container.innerHTML = '';
|
|
|
+
|
|
|
+ const cardsDiv = document.createElement('div');
|
|
|
+ cardsDiv.className = 'cards-area';
|
|
|
+ rangeData.forEach(card => {
|
|
|
+ if (placed[card.id]) return;
|
|
|
+ const el = document.createElement('div');
|
|
|
+ el.className = 'card' + (selectedCard === card.id ? ' selected' : '');
|
|
|
+ el.textContent = card.text;
|
|
|
+ el.onclick = () => {
|
|
|
+ selectedCard = selectedCard === card.id ? null : card.id;
|
|
|
+ render();
|
|
|
+ };
|
|
|
+ cardsDiv.appendChild(el);
|
|
|
+ });
|
|
|
+ container.appendChild(cardsDiv);
|
|
|
+
|
|
|
+ const binsDiv = document.createElement('div');
|
|
|
+ binsDiv.className = 'bins-area';
|
|
|
+ Object.keys(binNames).forEach(binKey => {
|
|
|
+ const bin = document.createElement('div');
|
|
|
+ bin.className = 'bin';
|
|
|
+ bin.innerHTML = '<span class="bin-title">' + binNames[binKey] + '</span>';
|
|
|
+ rangeData.filter(c => placed[c.id] === binKey).forEach(c => {
|
|
|
+ const item = document.createElement('span');
|
|
|
+ item.className = 'bin-item';
|
|
|
+ item.textContent = '✓ ' + c.text;
|
|
|
+ bin.appendChild(item);
|
|
|
+ });
|
|
|
+ bin.onclick = () => {
|
|
|
+ if (selectedCard === null) return;
|
|
|
+ const card = rangeData.find(c => c.id === selectedCard);
|
|
|
+ if (card.bin === binKey) {
|
|
|
+ placed[card.id] = binKey;
|
|
|
+ showFeedback(card.expl, true);
|
|
|
+ selectedCard = null;
|
|
|
+ } else {
|
|
|
+ showFeedback(card.expl, false);
|
|
|
+ }
|
|
|
+ render();
|
|
|
+ };
|
|
|
+ binsDiv.appendChild(bin);
|
|
|
+ });
|
|
|
+ container.appendChild(binsDiv);
|
|
|
+
|
|
|
+ if (Object.keys(placed).length === rangeData.length) {
|
|
|
+ const done = document.createElement('div');
|
|
|
+ done.className = 'feedback';
|
|
|
+ done.innerHTML = '<b>Все карточки разложены.</b> Почему СВЧ так часто? Высокая частота даёт широкую полосу и узкий луч, малая λ — высокое разрешение. Цена — прямая видимость и затухание в дожде.';
|
|
|
+ container.appendChild(done);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ render();
|
|
|
+}
|
|
|
+
|
|
|
+/* ============================================================
|
|
|
+ SCENE 8 — security
|
|
|
+ ============================================================ */
|
|
|
+function initSecurity() {
|
|
|
+ const canvas = document.getElementById('c-sec');
|
|
|
+ const state = { antenna: false, fhss: false, shield: false };
|
|
|
+
|
|
|
+ function computeRisks() {
|
|
|
+ const intercept = 85 - (state.antenna ? 30 : 0) - (state.fhss ? 15 : 0) - (state.shield ? 10 : 0);
|
|
|
+ const jam = 80 - (state.fhss ? 40 : 0) - (state.antenna ? 10 : 0);
|
|
|
+ const leak = 70 - (state.shield ? 40 : 0) - (state.antenna ? 10 : 0);
|
|
|
+ const clamp = x => Math.max(10, Math.min(90, x));
|
|
|
+ return { intercept: clamp(intercept), jam: clamp(jam), leak: clamp(leak) };
|
|
|
+ }
|
|
|
+
|
|
|
+ function draw() {
|
|
|
+ const { ctx, W, H } = prepCanvas(canvas, 260);
|
|
|
+ const risks = computeRisks();
|
|
|
+
|
|
|
+ const txX = 120, txY = 130;
|
|
|
+ const rxX = W - 120, rxY = 130;
|
|
|
+
|
|
|
+ ctx.strokeStyle = 'rgba(26,42,74,0.3)';
|
|
|
+ ctx.setLineDash([4, 4]);
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(txX, txY);
|
|
|
+ ctx.lineTo(rxX, rxY);
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.setLineDash([]);
|
|
|
+
|
|
|
+ const intR = state.antenna ? 50 : 110;
|
|
|
+ ctx.fillStyle = 'rgba(178,34,34,' + (0.10 + risks.intercept / 300) + ')';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.ellipse(txX, txY, intR, intR * 0.6, 0, 0, Math.PI * 2);
|
|
|
+ ctx.fill();
|
|
|
+ ctx.strokeStyle = 'rgba(178,34,34,0.5)';
|
|
|
+ ctx.setLineDash([3, 3]);
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.setLineDash([]);
|
|
|
+
|
|
|
+ const jamX = W / 2, jamY = 232;
|
|
|
+ ctx.fillStyle = '#8a2a2a';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.arc(jamX, jamY, 8, 0, Math.PI * 2);
|
|
|
+ ctx.fill();
|
|
|
+ ctx.fillStyle = '#1a2a4a';
|
|
|
+ ctx.font = '10px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.fillText('ПОМЕХА', jamX, jamY + 20);
|
|
|
+
|
|
|
+ if (state.fhss) {
|
|
|
+ ctx.strokeStyle = 'rgba(178,34,34,0.4)';
|
|
|
+ ctx.setLineDash([3, 3]);
|
|
|
+ for (let r = 20; r < 80; r += 20) {
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.arc(jamX, jamY, r, Math.PI * 1.1, Math.PI * 1.9);
|
|
|
+ ctx.stroke();
|
|
|
+ }
|
|
|
+ ctx.setLineDash([]);
|
|
|
+ } else {
|
|
|
+ ctx.strokeStyle = 'rgba(178,34,34,0.6)';
|
|
|
+ for (let r = 20; r < 100; r += 20) {
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.arc(jamX, jamY, r, Math.PI * 1.1, Math.PI * 1.9);
|
|
|
+ ctx.stroke();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function tower(x, y, label) {
|
|
|
+ ctx.fillStyle = '#1a2a4a';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x, y - 25);
|
|
|
+ ctx.lineTo(x - 8, y + 10);
|
|
|
+ ctx.lineTo(x + 8, y + 10);
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fill();
|
|
|
+ ctx.fillRect(x - 2, y + 10, 4, 15);
|
|
|
+ ctx.font = '10px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.fillText(label, x, y + 40);
|
|
|
+ }
|
|
|
+ tower(txX, txY, 'ПРД');
|
|
|
+ tower(rxX, rxY, 'ПРМ');
|
|
|
+
|
|
|
+ if (state.antenna) {
|
|
|
+ ctx.fillStyle = 'rgba(26,42,74,0.12)';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(txX, txY);
|
|
|
+ ctx.lineTo(rxX - 40, rxY - 40);
|
|
|
+ ctx.lineTo(rxX - 40, rxY + 40);
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fill();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (state.shield) {
|
|
|
+ ctx.strokeStyle = '#2a6a3a';
|
|
|
+ ctx.lineWidth = 3;
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.arc(txX, txY, 36, 0, Math.PI * 2);
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.lineWidth = 1;
|
|
|
+ ctx.fillStyle = '#2a6a3a';
|
|
|
+ ctx.font = '9px "Roboto Mono", monospace';
|
|
|
+ ctx.fillText('ЭКРАН', txX, txY - 42);
|
|
|
+ }
|
|
|
+
|
|
|
+ function colorFor(v) {
|
|
|
+ if (v > 60) return '#b22222';
|
|
|
+ if (v > 35) return '#d4a020';
|
|
|
+ return '#2a6a3a';
|
|
|
+ }
|
|
|
+ function bar(label, value) {
|
|
|
+ return '<div class="risk-row"><span class="risk-label">' + label +
|
|
|
+ '</span><div class="risk-track"><div class="risk-fill" style="width:' + value +
|
|
|
+ '%;background:' + colorFor(value) + '"></div></div><span style="min-width:32px;text-align:right">' +
|
|
|
+ value + '</span></div>';
|
|
|
+ }
|
|
|
+ document.getElementById('risk-bars').innerHTML =
|
|
|
+ bar('Перехват', risks.intercept) +
|
|
|
+ bar('Подавление', risks.jam) +
|
|
|
+ bar('Утечка', risks.leak);
|
|
|
+
|
|
|
+ const fb = [];
|
|
|
+ if (!state.antenna && !state.fhss && !state.shield) {
|
|
|
+ fb.push('Все меры выключены: широкая зона перехвата, помеха накрывает сигнал, утечка через корпус.');
|
|
|
+ }
|
|
|
+ if (state.antenna) fb.push('Направленная антенна сузила зону перехвата, но требует точного наведения.');
|
|
|
+ if (state.fhss) fb.push('ППРЧ: сигнал скачет по частотам, узкополосная помеха промахивается. От широкополосной не спасает.');
|
|
|
+ if (state.shield) fb.push('Экранирование снизило утечку, но не дальность полезного сигнала.');
|
|
|
+ document.getElementById('fb-sec').textContent = fb.join(' ');
|
|
|
+ }
|
|
|
+
|
|
|
+ document.querySelectorAll('#sec-toggles .toggle-item').forEach(el => {
|
|
|
+ el.onclick = () => {
|
|
|
+ const key = el.dataset.key;
|
|
|
+ state[key] = !state[key];
|
|
|
+ el.classList.toggle('on', state[key]);
|
|
|
+ draw();
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ currentDraw = draw;
|
|
|
+ draw();
|
|
|
+}
|
|
|
+
|
|
|
+/* ============================================================
|
|
|
+ SCENE 9 — test
|
|
|
+ ============================================================ */
|
|
|
+const testQuestions = [
|
|
|
+ {
|
|
|
+ q: 'λ = 30 м. Какая частота и диапазон?',
|
|
|
+ opts: ['10 МГц, КВ', '1 МГц, СВ', '100 МГц, УКВ', '30 кГц, ДВ'],
|
|
|
+ correct: 0,
|
|
|
+ expl: 'f = c/λ = 3·10⁸ / 30 = 10⁷ Гц = 10 МГц. Это КВ (3–30 МГц).'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ q: 'Почему ДВ слышно за горизонтом?',
|
|
|
+ opts: ['Ионосфера отражает', 'Дифракция: λ соизмерима с препятствиями', 'Земная волна идёт по прямой', 'Тропосферное рассеяние'],
|
|
|
+ correct: 1,
|
|
|
+ expl: 'λ на ДВ — сотни метров, соизмерима с холмами. Волна огибает препятствия (дифракция).'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ q: 'Почему УКВ не отражается от ионосферы?',
|
|
|
+ opts: ['Слишком большая мощность', 'Частота выше плазменной частоты слоя', 'Ионосфера есть только ночью', 'УКВ отражается, но мы не замечаем'],
|
|
|
+ correct: 1,
|
|
|
+ expl: 'Ионосфера отражает, пока f ниже плазменной частоты слоя. Для УКВ f выше — проходит насквозь.'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ q: 'Где полоса шире — на несущей 1 МГц или 100 МГц при той же F_м (модулирующем сигнале)?',
|
|
|
+ opts: ['На 1 МГц', 'На 100 МГц', 'Одинакова: B = 2F_м', 'Зависит от глубины модуляции'],
|
|
|
+ correct: 2,
|
|
|
+ expl: 'Полоса АМ-сигнала B = 2F_м зависит от модулирующего сигнала, не от несущей. Но на высокой несущей можно взять больший F_м.'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ q: 'Какой диапазон для связи со спутником?',
|
|
|
+ opts: ['ДВ', 'КВ', 'УКВ', 'СВЧ'],
|
|
|
+ correct: 3,
|
|
|
+ expl: 'СВЧ проходит сквозь ионосферу. КВ отражается, УКВ частично поглощается.'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ q: 'Что такое перемодуляция?',
|
|
|
+ opts: ['m = 0', 'm > 1, огибающая пересекает ноль', 'F_м > f_с', 'Полоса шире несущей'],
|
|
|
+ correct: 1,
|
|
|
+ expl: 'При m > 1 огибающая пересекает ноль. Приёмник искажает: не может восстановить сигнал.'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ q: 'Почему днём СВ хуже слышно на дальних расстояниях?',
|
|
|
+ opts: ['D-слой поглощает', 'Ионосфера исчезает', 'Антенна хуже работает', 'Помехи от Солнца'],
|
|
|
+ correct: 0,
|
|
|
+ expl: 'Днём D-слой ионизирован и поглощает СВ. Ночью D исчезает — дальний приём возвращается.'
|
|
|
+ }
|
|
|
+];
|
|
|
+
|
|
|
+function initTest() {
|
|
|
+ const container = document.getElementById('test-container');
|
|
|
+ container.innerHTML = '';
|
|
|
+ let answered = 0;
|
|
|
+ let correct = 0;
|
|
|
+ document.getElementById('checklist').style.display = 'none';
|
|
|
+
|
|
|
+ testQuestions.forEach((q, i) => {
|
|
|
+ const div = document.createElement('div');
|
|
|
+ div.className = 'question';
|
|
|
+ div.innerHTML =
|
|
|
+ '<div class="q-num">ВОПРОС ' + (i + 1) + ' / ' + testQuestions.length + '</div>' +
|
|
|
+ '<div class="q-text">' + q.q + '</div>' +
|
|
|
+ '<div class="options">' +
|
|
|
+ q.opts.map((o, j) => '<label><input type="radio" name="q' + i + '" value="' + j + '"> ' + o + '</label>').join('') +
|
|
|
+ '</div>' +
|
|
|
+ '<div class="explain" id="expl-' + i + '"></div>';
|
|
|
+ container.appendChild(div);
|
|
|
+
|
|
|
+ div.querySelectorAll('input').forEach(inp => {
|
|
|
+ inp.onchange = () => {
|
|
|
+ if (div.dataset.answered) return;
|
|
|
+ div.dataset.answered = '1';
|
|
|
+ const val = parseInt(inp.value, 10);
|
|
|
+ const ok = val === q.correct;
|
|
|
+ const expl = document.getElementById('expl-' + i);
|
|
|
+ expl.innerHTML = '<b>' + (ok ? 'Верно.' : 'Неверно.') + '</b> ' + q.expl;
|
|
|
+ expl.className = 'explain show' + (ok ? ' correct' : '');
|
|
|
+ div.querySelectorAll('input').forEach(r => r.disabled = true);
|
|
|
+ answered++;
|
|
|
+ if (ok) correct++;
|
|
|
+ updateScore();
|
|
|
+ };
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ function updateScore() {
|
|
|
+ const box = document.getElementById('score-box');
|
|
|
+ if (answered < testQuestions.length) {
|
|
|
+ box.textContent = 'Отвечено: ' + answered + ' / ' + testQuestions.length;
|
|
|
+ } else {
|
|
|
+ box.textContent = 'Результат: ' + correct + ' из ' + testQuestions.length;
|
|
|
+ document.getElementById('checklist').style.display = 'block';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ document.getElementById('score-box').textContent = 'Отвечено: 0 / ' + testQuestions.length;
|
|
|
+}
|
|
|
+
|
|
|
+/* ---------- init ---------- */
|
|
|
+goTo(1);
|
|
|
+</script>
|
|
|
+</body>
|
|
|
+</html>
|