|
|
@@ -0,0 +1,599 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="ru">
|
|
|
+<head>
|
|
|
+<meta charset="UTF-8">
|
|
|
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+<title>Объект 07 // Акустический Щит</title>
|
|
|
+<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
|
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
|
+<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=Roboto+Mono:wght@400;700&display=swap" rel="stylesheet">
|
|
|
+<style>
|
|
|
+ :root {
|
|
|
+ --wood-dark: #2b1d14;
|
|
|
+ --wood-light: #4a3222;
|
|
|
+ --bakelite: #1a1c19;
|
|
|
+ --brass: #b5a642;
|
|
|
+ --brass-dark: #8c7d2f;
|
|
|
+ --gold: #d4af37;
|
|
|
+ --soviet-red: #8b0000;
|
|
|
+ --crt-green: #39ff14;
|
|
|
+ --crt-dim: #1e3b1e;
|
|
|
+ --text-main: #e8dcc4;
|
|
|
+ }
|
|
|
+
|
|
|
+ * { box-sizing: border-box; }
|
|
|
+
|
|
|
+ body {
|
|
|
+ background-color: var(--wood-dark);
|
|
|
+ background-image:
|
|
|
+ repeating-linear-gradient(45deg, rgba(0,0,0,0.1) 0px, rgba(0,0,0,0.1) 2px, transparent 2px, transparent 4px),
|
|
|
+ radial-gradient(circle at center, #3a2516 0%, #1a100a 100%);
|
|
|
+ color: var(--text-main);
|
|
|
+ font-family: 'Playfair Display', serif;
|
|
|
+ padding: 20px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ min-height: 100vh;
|
|
|
+ margin: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-container {
|
|
|
+ background-color: var(--wood-light);
|
|
|
+ border: 6px solid var(--wood-dark);
|
|
|
+ border-radius: 8px;
|
|
|
+ width: 100%;
|
|
|
+ max-width: 680px;
|
|
|
+ padding: 25px;
|
|
|
+ box-shadow:
|
|
|
+ 0 20px 50px rgba(0,0,0,0.8),
|
|
|
+ inset 0 0 0 2px var(--brass-dark),
|
|
|
+ inset 0 0 20px rgba(0,0,0,0.8);
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-container::before, .device-container::after {
|
|
|
+ content: ''; position: absolute; width: 12px; height: 12px; border-radius: 50%;
|
|
|
+ background: radial-gradient(circle, var(--gold) 0%, var(--brass-dark) 100%);
|
|
|
+ box-shadow: inset -1px -1px 2px rgba(0,0,0,0.8), 1px 1px 2px rgba(0,0,0,0.5);
|
|
|
+ }
|
|
|
+ .device-container::before { top: 10px; left: 10px; }
|
|
|
+ .device-container::after { top: 10px; right: 10px; }
|
|
|
+
|
|
|
+ h1 {
|
|
|
+ text-align: center;
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: 900;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 3px;
|
|
|
+ color: var(--gold);
|
|
|
+ text-shadow: 2px 2px 0px #000, 0 0 10px rgba(212, 175, 55, 0.3);
|
|
|
+ margin-top: 0;
|
|
|
+ margin-bottom: 25px;
|
|
|
+ border-bottom: 3px double var(--brass);
|
|
|
+ padding-bottom: 15px;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ h1::after {
|
|
|
+ content: '★';
|
|
|
+ position: absolute;
|
|
|
+ bottom: -14px;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ background: var(--wood-light);
|
|
|
+ padding: 0 10px;
|
|
|
+ color: var(--soviet-red);
|
|
|
+ font-size: 18px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .scene { display: none; flex-direction: column; gap: 20px; }
|
|
|
+ .scene.active { display: flex; animation: fadeIn 0.4s ease-out; }
|
|
|
+
|
|
|
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
|
+
|
|
|
+ .panel {
|
|
|
+ background-color: var(--bakelite);
|
|
|
+ border: 2px solid var(--brass-dark);
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 20px;
|
|
|
+ box-shadow:
|
|
|
+ inset 0 0 20px rgba(0,0,0,0.9),
|
|
|
+ 0 4px 6px rgba(0,0,0,0.4);
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ .gauge-row {
|
|
|
+ display: flex; justify-content: space-between; align-items: center;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ padding-bottom: 10px;
|
|
|
+ border-bottom: 1px dashed var(--brass-dark);
|
|
|
+ }
|
|
|
+
|
|
|
+ .gauge-label {
|
|
|
+ font-family: 'Roboto Mono', monospace;
|
|
|
+ font-size: 13px;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 1px;
|
|
|
+ color: #a89f91;
|
|
|
+ }
|
|
|
+
|
|
|
+ .gauge-value {
|
|
|
+ font-family: 'Roboto Mono', monospace;
|
|
|
+ font-size: 22px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: var(--crt-green);
|
|
|
+ text-shadow: 0 0 8px rgba(57, 255, 20, 0.6);
|
|
|
+ background: #000;
|
|
|
+ padding: 4px 12px;
|
|
|
+ border-radius: 3px;
|
|
|
+ border: 1px solid #333;
|
|
|
+ box-shadow: inset 0 0 10px #000;
|
|
|
+ min-width: 90px;
|
|
|
+ text-align: right;
|
|
|
+ }
|
|
|
+
|
|
|
+ input[type=range] {
|
|
|
+ -webkit-appearance: none;
|
|
|
+ width: 100%;
|
|
|
+ background: transparent;
|
|
|
+ margin: 10px 0;
|
|
|
+ }
|
|
|
+ input[type=range]:focus { outline: none; }
|
|
|
+ input[type=range]::-webkit-slider-runnable-track {
|
|
|
+ width: 100%; height: 12px;
|
|
|
+ background: linear-gradient(to bottom, #111, #333);
|
|
|
+ border: 1px solid #000;
|
|
|
+ border-radius: 6px;
|
|
|
+ box-shadow: inset 0 2px 5px rgba(0,0,0,0.8);
|
|
|
+ }
|
|
|
+ input[type=range]::-webkit-slider-thumb {
|
|
|
+ -webkit-appearance: none;
|
|
|
+ height: 28px; width: 18px;
|
|
|
+ border-radius: 3px;
|
|
|
+ background: radial-gradient(circle at 30% 30%, #e6d380, var(--brass-dark));
|
|
|
+ border: 1px solid #000;
|
|
|
+ margin-top: -9px;
|
|
|
+ cursor: pointer;
|
|
|
+ box-shadow:
|
|
|
+ 0 3px 5px rgba(0,0,0,0.6),
|
|
|
+ inset 0 1px 1px rgba(255,255,255,0.5);
|
|
|
+ }
|
|
|
+
|
|
|
+ .switch-row {
|
|
|
+ display: flex; justify-content: space-between; align-items: center;
|
|
|
+ background: #111;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 4px;
|
|
|
+ border: 1px solid #333;
|
|
|
+ margin: 15px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .indicator {
|
|
|
+ width: 20px; height: 20px; border-radius: 50%;
|
|
|
+ background: #222; border: 2px solid #000;
|
|
|
+ box-shadow: inset 0 2px 4px rgba(0,0,0,0.8);
|
|
|
+ transition: all 0.3s;
|
|
|
+ }
|
|
|
+ .indicator.on {
|
|
|
+ background: var(--soviet-red);
|
|
|
+ box-shadow: 0 0 15px var(--soviet-red), inset 0 0 5px #ff6666;
|
|
|
+ border-color: #500000;
|
|
|
+ }
|
|
|
+
|
|
|
+ button {
|
|
|
+ background: linear-gradient(to bottom, #4a3222, #2b1d14);
|
|
|
+ color: var(--gold);
|
|
|
+ border: 2px solid var(--brass-dark);
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 12px 20px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-family: 'Playfair Display', serif;
|
|
|
+ font-weight: 700;
|
|
|
+ font-size: 16px;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 1px;
|
|
|
+ box-shadow:
|
|
|
+ 0 4px 0 #1a100a,
|
|
|
+ 0 5px 10px rgba(0,0,0,0.5);
|
|
|
+ transition: all 0.1s;
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+ button:active {
|
|
|
+ transform: translateY(4px);
|
|
|
+ box-shadow: 0 0 0 #1a100a, 0 2px 5px rgba(0,0,0,0.5);
|
|
|
+ }
|
|
|
+ button.start {
|
|
|
+ background: linear-gradient(to bottom, var(--soviet-red), #500000);
|
|
|
+ color: #fff;
|
|
|
+ border-color: #ff6666;
|
|
|
+ box-shadow: 0 4px 0 #300000, 0 5px 10px rgba(0,0,0,0.5);
|
|
|
+ }
|
|
|
+
|
|
|
+ .spy { font-size: 50px; text-align: center; transition: opacity 0.5s; filter: drop-shadow(0 0 10px rgba(0,0,0,0.8)); }
|
|
|
+
|
|
|
+ .formula-box {
|
|
|
+ background: #d4c9a8;
|
|
|
+ background-image: repeating-linear-gradient(0deg, transparent, transparent 19px, rgba(0,0,0,0.1) 20px);
|
|
|
+ color: #2b1d14;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 2px;
|
|
|
+ font-family: 'Roboto Mono', monospace;
|
|
|
+ font-size: 12px;
|
|
|
+ margin-top: 20px;
|
|
|
+ box-shadow: 2px 2px 5px rgba(0,0,0,0.5);
|
|
|
+ position: relative;
|
|
|
+ border: 1px solid #8c7d2f;
|
|
|
+ }
|
|
|
+ .formula-box::before {
|
|
|
+ content: 'УТВЕРЖДЕНО';
|
|
|
+ position: absolute;
|
|
|
+ top: 5px; right: 10px;
|
|
|
+ font-size: 10px;
|
|
|
+ color: var(--soviet-red);
|
|
|
+ border: 1px solid var(--soviet-red);
|
|
|
+ padding: 2px 5px;
|
|
|
+ transform: rotate(-10deg);
|
|
|
+ opacity: 0.7;
|
|
|
+ font-weight: bold;
|
|
|
+ }
|
|
|
+ .formula-box div { margin-bottom: 5px; font-weight: bold;}
|
|
|
+
|
|
|
+ .question { margin-bottom: 20px; font-family: 'Roboto Mono', monospace; font-size: 14px; }
|
|
|
+ .question label { display: block; margin-bottom: 8px; cursor: pointer; color: #ccc; }
|
|
|
+ .question input[type="radio"] { margin-right: 10px; accent-color: var(--soviet-red); }
|
|
|
+
|
|
|
+ hr { border: 0; height: 2px; background: var(--brass-dark); margin: 20px 0; box-shadow: 0 1px 0 #000; }
|
|
|
+</style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+<div class="device-container">
|
|
|
+ <h1>Объект 07 // Акустический Щит</h1>
|
|
|
+
|
|
|
+ <div id="scene1" class="scene active">
|
|
|
+ <div class="panel" style="text-align: center;">
|
|
|
+ <p style="font-size: 18px; margin-bottom: 20px;">Товарищ! В режимном помещении диктуется секретный пароль. За окном работает вражеский агент с направленным микрофоном.</p>
|
|
|
+ <div class="spy" id="spy1">🕵️♂️</div>
|
|
|
+ <button class="start" style="margin-top: 20px;" onclick="initAudio(); nextScene(2)">▶ Включить питание</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div id="scene2" class="scene">
|
|
|
+ <div class="panel">
|
|
|
+ <div class="gauge-row">
|
|
|
+ <span class="gauge-label">Дистанция до агента:</span>
|
|
|
+ <span class="gauge-value" id="distVal">1</span>
|
|
|
+ </div>
|
|
|
+ <input type="range" id="dist" min="1" max="10" value="1" oninput="onDistChange()">
|
|
|
+
|
|
|
+ <hr>
|
|
|
+
|
|
|
+ <div class="gauge-row">
|
|
|
+ <span class="gauge-label">Уровень сигнала (дБ):</span>
|
|
|
+ <span class="gauge-value" id="sigVal">0</span>
|
|
|
+ </div>
|
|
|
+ <div class="gauge-row" style="border: none; margin-bottom: 0; padding-bottom: 0;">
|
|
|
+ <span class="gauge-label">Громкость речи (%):</span>
|
|
|
+ <span class="gauge-value" id="volVal" style="color: var(--gold); text-shadow: 0 0 8px rgba(212, 175, 55, 0.6);">0</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <hr>
|
|
|
+
|
|
|
+ <div class="switch-row">
|
|
|
+ <span class="gauge-label">Генератор шума (Р-330):</span>
|
|
|
+ <div class="indicator" id="jamInd"></div>
|
|
|
+ </div>
|
|
|
+ <button id="jamBtn" onclick="toggleJam()" style="margin-bottom: 15px;">ВКЛ / ВЫКЛ</button>
|
|
|
+
|
|
|
+ <div class="gauge-row" style="margin-top: 20px;">
|
|
|
+ <span class="gauge-label">Мощность шума (дБ):</span>
|
|
|
+ <span class="gauge-value" id="noiseVal" style="color: var(--soviet-red); text-shadow: 0 0 8px rgba(139, 0, 0, 0.6);">0</span>
|
|
|
+ </div>
|
|
|
+ <input type="range" id="noise" min="0" max="100" value="0" oninput="onNoiseChange()">
|
|
|
+
|
|
|
+ <div class="gauge-row" style="border: none; margin-top: 20px;">
|
|
|
+ <span class="gauge-label">Отношение С/Ш (SNR):</span>
|
|
|
+ <span class="gauge-value" id="snrVal">0</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="formula-box">
|
|
|
+ <div>[РУКОВОДСТВО] Сигнал = 60 - 20 * log10(Расстояние)</div>
|
|
|
+ <div>[РУКОВОДСТВО] SNR = Сигнал - Шум</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <button onclick="playClick(); nextScene(3)">Перейти к оценке разборчивости ▶</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div id="scene3" class="scene">
|
|
|
+ <div class="panel">
|
|
|
+ <div class="gauge-row">
|
|
|
+ <span class="gauge-label">Разборчивость речи (%):</span>
|
|
|
+ <span class="gauge-value" id="intVal" style="font-size: 28px;">0</span>
|
|
|
+ </div>
|
|
|
+ <input type="range" id="noise2" min="0" max="100" value="0" oninput="onNoise2Change()">
|
|
|
+
|
|
|
+ <div class="spy" id="spy2" style="margin-top: 20px;">🕵️♂️</div>
|
|
|
+
|
|
|
+ <div class="formula-box">
|
|
|
+ <div>[РУКОВОДСТВО] R = 100 / (1 + exp(-0.5 * (SNR - 5)))</div>
|
|
|
+ <div>[ОГРАНИЧЕНИЕ] R = max(0, min(100, R))</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <button onclick="playClick(); nextScene(4)">Завершить испытания ▶</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div id="scene4" class="scene">
|
|
|
+ <div class="panel" id="testContainer">
|
|
|
+ <h3 style="margin-top:0; color: var(--gold); border-bottom: 1px solid var(--brass-dark); padding-bottom: 10px;">Итоговый контроль знаний</h3>
|
|
|
+ </div>
|
|
|
+ <button onclick="playClick(); checkTest()">Проверить ведомость</button>
|
|
|
+ </div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<script>
|
|
|
+ let dist = 1, noise = 0, isJamming = false;
|
|
|
+ let audioCtx, noiseSource, noiseGain;
|
|
|
+ let utterance, isSpeaking = false;
|
|
|
+
|
|
|
+ // Трекеры для щелчков (чтобы не трещало при быстром движении)
|
|
|
+ let lastDist = 1;
|
|
|
+ let lastNoise = 0;
|
|
|
+ let lastNoise2 = 0;
|
|
|
+
|
|
|
+ // --- Звук механического щелчка ---
|
|
|
+ function playClick() {
|
|
|
+ if (!audioCtx) return;
|
|
|
+ if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
|
+
|
|
|
+ const now = audioCtx.currentTime;
|
|
|
+ const bufferSize = Math.floor(audioCtx.sampleRate * 0.015);
|
|
|
+ const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
|
|
|
+ const data = buffer.getChannelData(0);
|
|
|
+
|
|
|
+ // Затухающий шумовой импульс
|
|
|
+ for (let i = 0; i < bufferSize; i++) {
|
|
|
+ data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufferSize, 6);
|
|
|
+ }
|
|
|
+
|
|
|
+ const src = audioCtx.createBufferSource();
|
|
|
+ src.buffer = buffer;
|
|
|
+
|
|
|
+ const filter = audioCtx.createBiquadFilter();
|
|
|
+ filter.type = 'bandpass';
|
|
|
+ filter.frequency.value = 2800;
|
|
|
+ filter.Q.value = 1.2;
|
|
|
+
|
|
|
+ const gain = audioCtx.createGain();
|
|
|
+ gain.gain.value = 0.25;
|
|
|
+
|
|
|
+ src.connect(filter);
|
|
|
+ filter.connect(gain);
|
|
|
+ gain.connect(audioCtx.destination);
|
|
|
+ src.start(now);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Щелчок потяжелее — для тумблера
|
|
|
+ function playSwitchClick() {
|
|
|
+ if (!audioCtx) return;
|
|
|
+ if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
|
+
|
|
|
+ const now = audioCtx.currentTime;
|
|
|
+ const bufferSize = Math.floor(audioCtx.sampleRate * 0.05);
|
|
|
+ const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
|
|
|
+ const data = buffer.getChannelData(0);
|
|
|
+ for (let i = 0; i < bufferSize; i++) {
|
|
|
+ data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufferSize, 4);
|
|
|
+ }
|
|
|
+
|
|
|
+ const src = audioCtx.createBufferSource();
|
|
|
+ src.buffer = buffer;
|
|
|
+
|
|
|
+ const filter = audioCtx.createBiquadFilter();
|
|
|
+ filter.type = 'lowpass';
|
|
|
+ filter.frequency.value = 1500;
|
|
|
+
|
|
|
+ const gain = audioCtx.createGain();
|
|
|
+ gain.gain.value = 0.4;
|
|
|
+
|
|
|
+ src.connect(filter);
|
|
|
+ filter.connect(gain);
|
|
|
+ gain.connect(audioCtx.destination);
|
|
|
+ src.start(now);
|
|
|
+ }
|
|
|
+
|
|
|
+ function initAudio() {
|
|
|
+ audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
|
+ const bufferSize = audioCtx.sampleRate * 2;
|
|
|
+ const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
|
|
|
+ const data = buffer.getChannelData(0);
|
|
|
+ for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
|
|
|
+
|
|
|
+ noiseSource = audioCtx.createBufferSource();
|
|
|
+ noiseSource.buffer = buffer;
|
|
|
+ noiseSource.loop = true;
|
|
|
+
|
|
|
+ noiseGain = audioCtx.createGain();
|
|
|
+ noiseGain.gain.value = 0;
|
|
|
+
|
|
|
+ noiseSource.connect(noiseGain);
|
|
|
+ noiseGain.connect(audioCtx.destination);
|
|
|
+ noiseSource.start();
|
|
|
+
|
|
|
+ utterance = new SpeechSynthesisUtterance("Пароль от сейфа: ноль, ноль, семь. Повторяю, ноль, ноль, семь.");
|
|
|
+ utterance.lang = 'ru-RU';
|
|
|
+ utterance.rate = 0.9;
|
|
|
+
|
|
|
+ utterance.onend = function() {
|
|
|
+ if (isSpeaking) {
|
|
|
+ setTimeout(() => {
|
|
|
+ if (isSpeaking) speechSynthesis.speak(utterance);
|
|
|
+ }, 1000);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ startSpeech();
|
|
|
+ playSwitchClick(); // Щелчок при включении питания
|
|
|
+ }
|
|
|
+
|
|
|
+ function startSpeech() {
|
|
|
+ if (!isSpeaking) {
|
|
|
+ isSpeaking = true;
|
|
|
+ speechSynthesis.cancel();
|
|
|
+ speechSynthesis.speak(utterance);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function nextScene(n) {
|
|
|
+ document.querySelectorAll('.scene').forEach(el => el.classList.remove('active'));
|
|
|
+ document.getElementById('scene' + n).classList.add('active');
|
|
|
+
|
|
|
+ if (n === 2) {
|
|
|
+ updateSignal();
|
|
|
+ updateNoise();
|
|
|
+ startSpeech();
|
|
|
+ }
|
|
|
+ if (n === 3) {
|
|
|
+ document.getElementById('noise2').value = noise;
|
|
|
+ lastNoise2 = noise;
|
|
|
+ updateInt();
|
|
|
+ startSpeech();
|
|
|
+ }
|
|
|
+ if (n === 4) {
|
|
|
+ isSpeaking = false;
|
|
|
+ speechSynthesis.cancel();
|
|
|
+ if (noiseGain) noiseGain.gain.value = 0;
|
|
|
+ renderTest();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- Обработчики с щелчками ---
|
|
|
+ function onDistChange() {
|
|
|
+ const newVal = parseFloat(document.getElementById('dist').value);
|
|
|
+ if (newVal !== lastDist) {
|
|
|
+ playClick();
|
|
|
+ lastDist = newVal;
|
|
|
+ }
|
|
|
+ updateSignal();
|
|
|
+ }
|
|
|
+
|
|
|
+ function onNoiseChange() {
|
|
|
+ const newVal = parseFloat(document.getElementById('noise').value);
|
|
|
+ if (newVal !== lastNoise) {
|
|
|
+ playClick();
|
|
|
+ lastNoise = newVal;
|
|
|
+ }
|
|
|
+ updateNoise();
|
|
|
+ }
|
|
|
+
|
|
|
+ function onNoise2Change() {
|
|
|
+ const newVal = parseFloat(document.getElementById('noise2').value);
|
|
|
+ if (newVal !== lastNoise2) {
|
|
|
+ playClick();
|
|
|
+ lastNoise2 = newVal;
|
|
|
+ }
|
|
|
+ updateInt();
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateSignal() {
|
|
|
+ dist = parseFloat(document.getElementById('dist').value);
|
|
|
+ document.getElementById('distVal').innerText = dist;
|
|
|
+
|
|
|
+ let signal = 60 - (20 * Math.log10(dist));
|
|
|
+ document.getElementById('sigVal').innerText = signal.toFixed(1);
|
|
|
+
|
|
|
+ let vol = Math.max(0, Math.min(1, signal / 60));
|
|
|
+ document.getElementById('volVal').innerText = Math.round(vol * 100);
|
|
|
+
|
|
|
+ if (utterance) {
|
|
|
+ utterance.volume = vol;
|
|
|
+ if (isSpeaking && speechSynthesis.speaking) {
|
|
|
+ speechSynthesis.cancel();
|
|
|
+ speechSynthesis.speak(utterance);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ updateSNR();
|
|
|
+ }
|
|
|
+
|
|
|
+ function toggleJam() {
|
|
|
+ playSwitchClick();
|
|
|
+ isJamming = !isJamming;
|
|
|
+ document.getElementById('jamInd').classList.toggle('on', isJamming);
|
|
|
+ document.getElementById('jamBtn').innerText = isJamming ? 'ОСТАНОВИТЬ' : 'ВКЛ / ВЫКЛ';
|
|
|
+
|
|
|
+ if (isJamming) {
|
|
|
+ if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
|
+ startSpeech();
|
|
|
+ }
|
|
|
+ updateNoise();
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateNoise() {
|
|
|
+ noise = isJamming ? parseFloat(document.getElementById('noise').value) : 0;
|
|
|
+ document.getElementById('noiseVal').innerText = noise;
|
|
|
+
|
|
|
+ if (noiseGain) noiseGain.gain.setTargetAtTime(noise / 100, audioCtx.currentTime, 0.1);
|
|
|
+ updateSNR();
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateSNR() {
|
|
|
+ let d = parseFloat(document.getElementById('dist').value);
|
|
|
+ let signal = 60 - (20 * Math.log10(d));
|
|
|
+ let currentNoise = isJamming ? parseFloat(document.getElementById('noise').value) : 0;
|
|
|
+ document.getElementById('snrVal').innerText = (signal - currentNoise).toFixed(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateInt() {
|
|
|
+ let d = parseFloat(document.getElementById('dist').value);
|
|
|
+ let n = parseFloat(document.getElementById('noise2').value);
|
|
|
+ document.getElementById('noiseVal').innerText = n;
|
|
|
+
|
|
|
+ let signal = 60 - (20 * Math.log10(d));
|
|
|
+ let snr = signal - n;
|
|
|
+
|
|
|
+ let intelligibility = 100 / (1 + Math.exp(-0.5 * (snr - 5)));
|
|
|
+ intelligibility = Math.max(0, Math.min(100, intelligibility));
|
|
|
+
|
|
|
+ document.getElementById('intVal').innerText = intelligibility.toFixed(1);
|
|
|
+
|
|
|
+ if (noiseGain) noiseGain.gain.setTargetAtTime(n / 100, audioCtx.currentTime, 0.1);
|
|
|
+
|
|
|
+ if (intelligibility < 20) {
|
|
|
+ document.getElementById('spy2').style.opacity = '0.1';
|
|
|
+ document.getElementById('spy2').innerText = '💨';
|
|
|
+ } else {
|
|
|
+ document.getElementById('spy2').style.opacity = '1';
|
|
|
+ document.getElementById('spy2').innerText = '🕵️♂️';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const questions = [
|
|
|
+ { q: "Что такое SNR?", a: ["Отношение сигнал/шум", "Скорость передачи", "Уровень сигнала"], c: 0 },
|
|
|
+ { q: "Как затухает звук от расстояния?", a: ["Линейно", "По логарифмическому закону", "Не затухает"], c: 1 },
|
|
|
+ { q: "Зачем нужен генератор шума?", a: ["Для музыки", "Для маскировки речи", "Для увеличения громкости"], c: 1 },
|
|
|
+ { q: "Если шум больше сигнала, то...", a: ["Разборчивость падает", "Разборчивость растет", "Ничего не меняется"], c: 0 },
|
|
|
+ { q: "Разборчивость может быть отрицательной?", a: ["Да", "Нет", "Иногда"], c: 1 }
|
|
|
+ ];
|
|
|
+
|
|
|
+ function renderTest() {
|
|
|
+ let html = '';
|
|
|
+ questions.forEach((item, i) => {
|
|
|
+ html += `<div class="question"><p style="color:var(--gold); margin-bottom:10px;"><b>${i+1}. ${item.q}</b></p>`;
|
|
|
+ item.a.forEach((ans, j) => {
|
|
|
+ html += `<label><input type="radio" name="q${i}" value="${j}" onchange="playClick()"> ${ans}</label>`;
|
|
|
+ });
|
|
|
+ html += `</div>`;
|
|
|
+ });
|
|
|
+ document.getElementById('testContainer').innerHTML = `<h3 style="margin-top:0; color: var(--gold); border-bottom: 1px solid var(--brass-dark); padding-bottom: 10px;">Итоговый контроль знаний</h3>` + html;
|
|
|
+ }
|
|
|
+
|
|
|
+ function checkTest() {
|
|
|
+ let score = 0;
|
|
|
+ questions.forEach((item, i) => {
|
|
|
+ const selected = document.querySelector(`input[name="q${i}"]:checked`);
|
|
|
+ if (selected && parseInt(selected.value) === item.c) score++;
|
|
|
+ });
|
|
|
+ alert(`Результат: ${score} из ${questions.length}. ${score === 5 ? 'Отлично, товарищ!' : 'Требуется повторный инструктаж.'}`);
|
|
|
+ }
|
|
|
+</script>
|
|
|
+</body>
|
|
|
+</html>
|