瀏覽代碼

Удалить 'MICRO.html'

u23-27burdyko 1 周之前
父節點
當前提交
62d30b5d04
共有 1 個文件被更改,包括 0 次插入 和 1543 次删除
  1. 0 1543
      MICRO.html

+ 0 - 1543
MICRO.html

@@ -1,1543 +0,0 @@
-<!DOCTYPE html>
-<html lang="ru">
-<head>
-<meta charset="UTF-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Акустоэлектрическое преобразование — интерактивная лаборатория</title>
-<style>
-  :root{
-    --bg-1:#1a1005; --bg-2:#0a0603;
-    --amber:#d4a860; --gold:#f0d890; --terra:#c87850;
-    --khaki:#b8c878; --cream:#e8d0a0; --rust:#b86848; --olive:#8a9860;
-  }
-  *{box-sizing:border-box}
-  html{scroll-behavior:smooth}
-  body{
-    margin:0;
-    background:
-      radial-gradient(circle at 20% 0%, #2a1a0e 0%, transparent 55%),
-      radial-gradient(circle at 80% 100%, #1a2218 0%, transparent 55%),
-      #0a0603;
-    color:var(--cream);
-    font-family:"Courier New","JetBrains Mono",monospace;
-    font-size:15px; line-height:1.7; overflow-x:hidden;
-  }
-  body::before{
-    content:""; position:fixed; inset:0;
-    background-image:
-      linear-gradient(rgba(212,168,96,.03) 1px, transparent 1px),
-      linear-gradient(90deg, rgba(212,168,96,.03) 1px, transparent 1px);
-    background-size:40px 40px; pointer-events:none; z-index:0;
-  }
-
-  .topbar{
-    position:fixed; top:0; left:0; right:0;
-    background:linear-gradient(180deg, rgba(20,12,6,.95), rgba(20,12,6,.75));
-    backdrop-filter:blur(8px);
-    border-bottom:1px solid rgba(212,168,96,.25);
-    padding:8px 20px;
-    display:flex; justify-content:space-between; align-items:center;
-    z-index:100; font-size:12.5px; letter-spacing:1px;
-    gap:12px; flex-wrap:wrap;
-  }
-  .topbar .brand{
-    color:var(--amber); font-weight:bold;
-    text-shadow:0 0 10px rgba(212,168,96,.6);
-    flex-shrink:0;
-  }
-  .topbar .controls{display:flex; gap:8px; align-items:center; flex-wrap:wrap}
-  .topbar button{
-    background:rgba(212,168,96,.1); color:var(--amber);
-    border:1px solid rgba(212,168,96,.4);
-    padding:5px 12px; font-family:inherit; font-size:11.5px;
-    letter-spacing:1px; cursor:pointer;
-    transition:background .2s; border-radius:2px;
-  }
-  .topbar button:hover{background:rgba(212,168,96,.22)}
-  .topbar button.active{background:var(--amber); color:#1a1005}
-  .scroll-progress{
-    position:absolute; bottom:-1px; left:0; height:2px;
-    background:linear-gradient(90deg,var(--terra),var(--amber),var(--gold));
-    width:0%; box-shadow:0 0 8px var(--amber);
-    transition:width .1s linear;
-  }
-
-  .scene{
-    position:relative; z-index:1;
-    max-width:980px; margin:0 auto; padding:80px 24px;
-    opacity:0; transform:translateY(40px);
-    transition:opacity .9s cubic-bezier(.2,.8,.3,1), transform .9s cubic-bezier(.2,.8,.3,1);
-  }
-  .scene.in-view{opacity:1; transform:translateY(0)}
-
-  .scene-header{
-    display:flex; align-items:baseline; gap:20px;
-    margin-bottom:24px; padding-bottom:14px;
-    border-bottom:1px solid rgba(212,168,96,.2);
-  }
-  .scene-num{
-    font-size:56px; color:var(--terra); font-weight:bold;
-    line-height:1; text-shadow:0 0 24px rgba(200,120,80,.5);
-    letter-spacing:-2px;
-  }
-  .scene-title{
-    font-size:24px; color:var(--amber);
-    text-shadow:0 0 16px rgba(212,168,96,.5);
-    letter-spacing:1px; margin:0; font-weight:bold;
-  }
-  .scene-text{
-    font-size:15.5px; color:var(--cream);
-    margin:0 0 24px 0; max-width:720px;
-  }
-  .scene-text p{margin:0 0 14px 0}
-  .scene-text strong{color:var(--gold); font-weight:bold}
-  .scene-text em{color:var(--khaki); font-style:normal}
-
-  .screen-frame{
-    background:#000;
-    border:2px solid rgba(212,168,96,.35);
-    border-radius:3px;
-    overflow:hidden; position:relative;
-    box-shadow:0 0 0 1px rgba(0,0,0,.8), 0 0 40px rgba(212,168,96,.1);
-  }
-  .screen-frame::after{
-    content:""; position:absolute; inset:0;
-    background:repeating-linear-gradient(to bottom,
-      transparent 0, transparent 2px,
-      rgba(0,0,0,.18) 3px, rgba(0,0,0,.18) 4px);
-    pointer-events:none; z-index:2;
-  }
-  .screen-frame::before{
-    content:""; position:absolute; inset:0;
-    background:radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,.5) 100%);
-    pointer-events:none; z-index:3;
-  }
-  canvas{display:block; width:100%; height:auto; position:relative; z-index:1}
-
-  .interact-hint{
-    margin-top:10px; padding:8px 14px;
-    background:rgba(212,168,96,.06);
-    border-left:2px solid var(--terra);
-    color:rgba(232,208,160,.85);
-    font-size:12.5px; letter-spacing:.5px;
-  }
-  .interact-hint::before{
-    content:"▸ "; color:var(--terra); font-weight:bold;
-  }
-
-  .formula{
-    display:inline-block;
-    background:rgba(212,168,96,.08);
-    border:1px solid rgba(212,168,96,.35);
-    padding:10px 20px; margin:14px 0;
-    color:var(--gold); font-size:20px;
-    letter-spacing:2px; border-radius:2px;
-    text-shadow:0 0 12px rgba(240,216,144,.5);
-  }
-  .formula small{
-    display:block; font-size:12px;
-    color:rgba(232,208,160,.7);
-    letter-spacing:1px; margin-top:6px; text-shadow:none;
-  }
-
-  .hero{
-    min-height:100vh; display:flex; flex-direction:column;
-    justify-content:center; align-items:center;
-    text-align:center; padding:80px 24px;
-    position:relative; z-index:1;
-    opacity:1; transform:none;
-  }
-  .hero h1{
-    font-size:clamp(28px, 5vw, 52px);
-    color:var(--amber); margin:0 0 20px 0;
-    letter-spacing:3px;
-    text-shadow:0 0 20px rgba(212,168,96,.7), 0 0 60px rgba(212,168,96,.3);
-    line-height:1.1;
-  }
-  .hero h2{
-    font-size:clamp(14px, 2vw, 18px);
-    color:var(--khaki); letter-spacing:2px;
-    font-weight:normal; margin:0 0 40px 0; opacity:.85;
-  }
-  .hero .divider{
-    width:120px; height:1px;
-    background:linear-gradient(90deg, transparent, var(--amber), transparent);
-    margin:24px 0; box-shadow:0 0 12px var(--amber);
-  }
-  .hero .meta{
-    display:flex; gap:36px; flex-wrap:wrap; justify-content:center;
-    color:rgba(232,208,160,.6);
-    font-size:12px; letter-spacing:1.5px; margin-top:32px;
-  }
-  .hero .meta span{display:flex; gap:8px; align-items:center}
-  .hero .meta b{color:var(--amber)}
-  .hero .howto{
-    margin-top:48px; color:rgba(232,208,160,.55);
-    font-size:12.5px; letter-spacing:1px;
-    max-width:520px;
-  }
-  .hero .howto b{color:var(--terra)}
-
-  .scroll-hint{
-    position:absolute; bottom:36px;
-    left:50%; transform:translateX(-50%);
-    color:var(--amber); font-size:11px; letter-spacing:2px;
-    opacity:.7; animation:bob 2s ease-in-out infinite;
-  }
-  @keyframes bob{
-    0%,100%{transform:translate(-50%, 0)}
-    50%{transform:translate(-50%, 10px)}
-  }
-  .scroll-hint::after{
-    content:"▼"; display:block; text-align:center; margin-top:6px;
-    color:var(--terra); font-size:14px;
-  }
-
-  .lab-tabs{display:flex; gap:6px; margin-bottom:12px; flex-wrap:wrap}
-  .lab-tab{
-    background:rgba(212,168,96,.08); color:var(--amber);
-    border:1px solid rgba(212,168,96,.3);
-    padding:8px 18px; font-family:inherit; font-size:13px;
-    letter-spacing:1px; cursor:pointer;
-    transition:all .2s; border-radius:2px;
-  }
-  .lab-tab:hover{background:rgba(212,168,96,.18)}
-  .lab-tab.active{
-    background:var(--amber); color:#1a1005;
-    box-shadow:0 0 20px rgba(212,168,96,.5);
-  }
-
-  .lab-panel{
-    display:none; padding:14px 18px;
-    background:rgba(20,12,6,.6);
-    border:1px solid rgba(212,168,96,.25);
-    margin-top:12px; border-radius:2px;
-    flex-wrap:wrap; gap:14px; align-items:center;
-  }
-  .lab-panel.on{display:flex}
-  .lab-panel label{
-    font-size:12.5px; color:var(--cream);
-    display:flex; align-items:center; gap:8px; letter-spacing:.5px;
-  }
-  .lab-panel input[type=range]{width:130px; accent-color:var(--terra)}
-  .lab-panel select, .lab-panel input[type=text]{
-    background:rgba(0,0,0,.5); color:var(--gold);
-    border:1px solid rgba(212,168,96,.4);
-    padding:5px 10px; font-family:inherit; font-size:13px;
-    border-radius:2px; outline:none;
-  }
-  .lab-panel select option{background:#1a1005}
-  .lab-panel .value{
-    color:var(--amber); font-family:"Courier New",monospace;
-    font-size:12px; min-width:52px; text-align:right;
-  }
-  .lab-btn{
-    background:rgba(212,168,96,.1); color:var(--amber);
-    border:1px solid rgba(212,168,96,.4);
-    padding:7px 16px; font-family:inherit; font-size:12.5px;
-    letter-spacing:1px; cursor:pointer;
-    border-radius:2px; transition:all .2s;
-  }
-  .lab-btn:hover{background:rgba(212,168,96,.22)}
-  .lab-btn.primary{background:var(--terra); color:#f0f8e0; border-color:var(--terra)}
-  .lab-btn.primary:hover{background:#d88858}
-  .lab-btn.rec{background:rgba(200,72,72,.5); color:#fff; border-color:rgba(200,72,72,.7)}
-  .lab-btn.rec.on{background:#c84848; animation:recPulse 1s infinite}
-  @keyframes recPulse{50%{background:#8a2828}}
-  .lab-btn:disabled{opacity:.4; cursor:default}
-
-  .chip-row{display:flex; flex-wrap:wrap; gap:6px; margin-top:10px}
-  .chip{
-    background:rgba(212,168,96,.06);
-    color:rgba(232,208,160,.85);
-    border:1px solid rgba(212,168,96,.25);
-    padding:5px 12px; font-family:inherit; font-size:11.5px;
-    cursor:pointer; border-radius:2px; transition:all .2s;
-  }
-  .chip:hover{background:rgba(212,168,96,.18); color:var(--gold)}
-
-  footer{
-    position:relative; z-index:1;
-    text-align:center; padding:80px 24px 100px;
-    border-top:1px solid rgba(212,168,96,.2);
-    margin-top:80px;
-  }
-  footer h3{
-    color:var(--amber); font-size:22px; letter-spacing:2px;
-    text-shadow:0 0 16px rgba(212,168,96,.5);
-    margin:0 0 24px 0;
-  }
-  footer ul{list-style:none; padding:0; max-width:640px; margin:0 auto 32px; text-align:left}
-  footer li{
-    padding:10px 0 10px 28px; position:relative;
-    color:var(--cream); font-size:14px;
-  }
-  footer li::before{content:"▸"; position:absolute; left:0; top:10px; color:var(--terra)}
-  footer .sig{color:rgba(232,208,160,.4); font-size:11.5px; letter-spacing:1.5px; margin-top:24px}
-
-  @media (max-width:640px){
-    .topbar{padding:6px 12px; font-size:11px}
-    .topbar .brand{font-size:10.5px; letter-spacing:1px; max-width:60%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
-    .scene{padding:56px 16px}
-    .scene-num{font-size:40px}
-    .scene-title{font-size:19px}
-    .scene-text{font-size:14px}
-    .lab-tab{padding:6px 12px; font-size:11.5px}
-    .lab-panel{padding:10px 12px; gap:10px}
-    .lab-panel input[type=range]{width:90px}
-  }
-</style>
-</head>
-<body>
-
-<div class="topbar">
-  <div class="brand">◤ ACOUSTO-LAB</div>
-  <div class="controls">
-    <button id="readSceneBtn" title="Озвучить текущую главу">📖 ЧИТАТЬ ГЛАВУ</button>
-    <button id="voiceToggle" class="active">🔊 ГОЛОС</button>
-    <button id="stopBtn" title="Стоп">■</button>
-    <button id="scrollTopBtn" title="Наверх">↑</button>
-  </div>
-  <div class="scroll-progress" id="scrollProgress"></div>
-</div>
-
-<header class="hero" id="hero">
-  <h1>АКУСТОЭЛЕКТРИЧЕСКОЕ<br>ПРЕОБРАЗОВАНИЕ</h1>
-  <div class="divider"></div>
-  <h2>ИНТЕРАКТИВНАЯ ЛАБОРАТОРИЯ · КАК ЗВУК СТАНОВИТСЯ ЭЛЕКТРИЧЕСТВОМ</h2>
-  <div class="meta">
-    <span><b>01</b> Звуковая волна</span>
-    <span><b>02</b> Диафрагма</span>
-    <span><b>03</b> Индукция</span>
-    <span><b>04</b> ЭДС</span>
-    <span><b>05</b> Сигнал</span>
-    <span><b>06</b> Лаборатория</span>
-  </div>
-  <div class="howto">
-    В каждой главе — <b>свой интерактив</b>: тяни ползунки, щёлкай по мембране, тащи катушку.
-    Кнопка <b>«📖 ЧИТАТЬ ГЛАВУ»</b> в шапке озвучит текст той главы, что сейчас на экране.
-  </div>
-  <div class="scroll-hint">СКРОЛЛ</div>
-</header>
-
-<!-- ГЛАВА 1 -->
-<section class="scene" id="scene-sound">
-  <div class="scene-header">
-    <div class="scene-num">01</div>
-    <h2 class="scene-title">Звук — это колебания воздуха</h2>
-  </div>
-  <div class="scene-text">
-    <p>Когда вы говорите, ваши голосовые связки создают <strong>колебания давления</strong> в воздухе. Молекулы воздуха начинают сжиматься и разжиматься, и эта волна распространяется во все стороны со скоростью около <em>340 м/с</em>.</p>
-    <p>Звук — это не «что-то материальное», что летит от рта к уху. Это <strong>процесс</strong>: волна плотности, бегущая сквозь среду. Каждая частица воздуха колеблется на месте, но передаёт толчок следующей.</p>
-  </div>
-  <div class="screen-frame">
-    <canvas id="cv-sound" data-h="300"></canvas>
-  </div>
-  <div class="interact-hint">Тяни ползунок громкости внизу экрана — увидишь, как меняется амплитуда и размер молекул.</div>
-</section>
-
-<!-- ГЛАВА 2 -->
-<section class="scene" id="scene-membrane">
-  <div class="scene-header">
-    <div class="scene-num">02</div>
-    <h2 class="scene-title">Диафрагма ловит волну</h2>
-  </div>
-  <div class="scene-text">
-    <p>Внутри микрофона находится тонкая <strong>мембрана</strong> — диафрагма. Когда звуковая волна ударяет по ней, мембрана начинает колебаться в такт с частотой звука. Чем громче звук — тем больше амплитуда колебаний.</p>
-    <p>Это ещё <em>механика</em>: акустическая энергия превращается в механическое движение. Самое главное здесь — мембрана повторяет форму волны <strong>без искажений</strong>.</p>
-  </div>
-  <div class="screen-frame">
-    <canvas id="cv-membrane" data-h="320"></canvas>
-  </div>
-  <div class="interact-hint">Щёлкни по мембране — толкни её рукой и посмотри, как она качнётся и вернётся.</div>
-</section>
-
-<!-- ГЛАВА 3 -->
-<section class="scene" id="scene-coil">
-  <div class="scene-header">
-    <div class="scene-num">03</div>
-    <h2 class="scene-title">Катушка в магнитном поле</h2>
-  </div>
-  <div class="scene-text">
-    <p>К мембране жёстко прикреплена <strong>катушка</strong> из тонкого провода. Она находится в зазоре между полюсами постоянного магнита <em>N</em> и <em>S</em>. Когда мембрана колеблется — катушка движется в магнитном поле.</p>
-    <p>Согласно закону электромагнитной индукции, движение проводника в магнитном поле создаёт в нём <strong>электрический ток</strong>. Это ключевой момент всего преобразования.</p>
-  </div>
-  <div class="screen-frame">
-    <canvas id="cv-coil" data-h="360"></canvas>
-  </div>
-  <div class="interact-hint">Потяни катушку мышью влево-вправо — стрелка гальванометра отреагирует на движение.</div>
-</section>
-
-<!-- ГЛАВА 4 -->
-<section class="scene" id="scene-faraday">
-  <div class="scene-header">
-    <div class="scene-num">04</div>
-    <h2 class="scene-title">Закон Фарадея</h2>
-  </div>
-  <div class="scene-text">
-    <p>Величина возникающей ЭДС пропорциональна <strong>скорости изменения магнитного потока</strong>. Быстрее движется катушка — сильнее ток. Медленнее — слабее. Стоит на месте — тока нет вообще.</p>
-    <div class="formula">
-      ε = − dΦ / dt
-      <small>ЭДС индукции = минус скорость изменения магнитного потока</small>
-    </div>
-    <p>Именно поэтому микрофон не может «записать» постоянный звук — ему нужно <strong>изменение</strong>. Тишина на входе = ноль на выходе.</p>
-  </div>
-  <div class="screen-frame">
-    <canvas id="cv-faraday" data-h="340"></canvas>
-  </div>
-  <div class="interact-hint">Нажми на кнопку «МАГНИТ: ВКЛ / ВЫКЛ» прямо на экране — увидишь, как гаснет ЭДС без поля.</div>
-</section>
-
-<!-- ГЛАВА 5 -->
-<section class="scene" id="scene-signal">
-  <div class="scene-header">
-    <div class="scene-num">05</div>
-    <h2 class="scene-title">Электрический сигнал</h2>
-  </div>
-  <div class="scene-text">
-    <p>На выходе микрофона мы получаем <strong>электрический сигнал</strong> — переменное напряжение, форма которого в точности повторяет форму звуковой волны. Это и есть акустоэлектрическое преобразование.</p>
-    <p>Сигнал очень слабый — <em>милливольты</em>. Его усиливают, оцифровывают и передают дальше: в колонки, в наушники, в запись. Один и тот же принцип работает в телефоне, диктофоне, слуховом аппарате, гидрофоне.</p>
-  </div>
-  <div class="screen-frame">
-    <canvas id="cv-signal" data-h="280"></canvas>
-  </div>
-  <div class="interact-hint">Двигай ползунок усиления — увидишь, как растёт и сжимается сигнал. Кнопка «ШУМ» добавит помехи.</div>
-</section>
-
-<!-- ГЛАВА 6 · ЛАБОРАТОРИЯ -->
-<section class="scene" id="scene-lab">
-  <div class="scene-header">
-    <div class="scene-num">06</div>
-    <h2 class="scene-title">Лаборатория</h2>
-  </div>
-  <div class="scene-text">
-    <p>Здесь можно управлять сигналом: генерировать волну любой формы, ловить её с микрофона или озвучивать текст. Спектрограмма внизу показывает <strong>историю частот</strong> — как в настоящих звуковых редакторах.</p>
-  </div>
-
-  <div class="lab-tabs">
-    <button class="lab-tab active" data-mode="imitation">◉ Генератор</button>
-    <button class="lab-tab" data-mode="mic">🎤 Микрофон</button>
-    <button class="lab-tab" data-mode="text">⌨ Текст</button>
-  </div>
-
-  <div class="screen-frame">
-    <canvas id="cv-lab" data-h="420"></canvas>
-  </div>
-
-  <div class="lab-panel on" id="panel-imitation">
-    <label>Частота <input type="range" id="freqRange" min="60" max="400" value="140"><span class="value" id="freqVal">140 Гц</span></label>
-    <label>Амплитуда <input type="range" id="ampRange" min="10" max="100" value="60"><span class="value" id="ampVal">60%</span></label>
-    <label>Форма
-      <select id="waveSelect">
-        <option value="sine">синус</option>
-        <option value="saw">пила</option>
-        <option value="square">квадрат</option>
-        <option value="noise">шум</option>
-        <option value="voice">голос</option>
-      </select>
-    </label>
-    <label>Модуляция <input type="range" id="modRange" min="0" max="100" value="30"><span class="value" id="modVal">30%</span></label>
-  </div>
-
-  <div class="lab-panel" id="panel-mic">
-    <button class="lab-btn rec" id="recBtn">● ЗАПИСАТЬ</button>
-    <button class="lab-btn" id="playBtn" disabled>▶ ВОСПРОИЗВЕСТИ</button>
-    <span style="color:var(--cream); font-size:12px">Уровень: <span id="levelVal" class="value">0%</span></span>
-  </div>
-
-  <div class="lab-panel" id="panel-text">
-    <div style="width:100%; display:flex; gap:10px; flex-wrap:wrap">
-      <input type="text" id="textInput" placeholder="Напиши фразу и нажми Enter…" style="flex:1; min-width:200px">
-      <button class="lab-btn primary" id="speakInputBtn">▶ ОЗВУЧИТЬ</button>
-    </div>
-    <div class="chip-row" style="width:100%">
-      <button class="chip" data-phrase="Звук — это колебания давления воздуха.">звук</button>
-      <button class="chip" data-phrase="Катушка движется в магнитном поле, и возникает ток.">индукция</button>
-      <button class="chip" data-phrase="Так микрофон превращает голос в электрический сигнал.">микрофон</button>
-      <button class="chip" data-phrase="Проверка, раз, два, три.">проверка</button>
-    </div>
-  </div>
-</section>
-
-<footer>
-  <h3>ИТОГИ</h3>
-  <ul>
-    <li>Звук — это колебания давления воздуха, распространяющиеся как волна.</li>
-    <li>Диафрагма микрофона механически повторяет эти колебания.</li>
-    <li>Катушка, прикреплённая к диафрагме, движется в магнитном поле.</li>
-    <li>По закону Фарадея возникает ЭДС индукции — переменное напряжение.</li>
-    <li>Этот сигнал — точная электрическая копия исходного звука.</li>
-    <li>Тот же принцип работает в телефонах, диктофонах, гидрофонах и слуховых аппаратах.</li>
-  </ul>
-  <div class="sig">ACOUSTO-LAB.EXE · интерактивный проект · 2025</div>
-</footer>
-
-<script>
-/* =========================================================
-   ОБЩИЕ УТИЛИТЫ
-   ========================================================= */
-const C = {
-  amber:'#d4a860', gold:'#f0d890', terra:'#c87850',
-  khaki:'#b8c878', cream:'#e8d0a0', rust:'#b86848',
-  olive:'#8a9860', bg:'#1a1005'
-};
-
-function rr(ctx,x,y,w,h,r){
-  ctx.beginPath();
-  ctx.moveTo(x+r,y);
-  ctx.arcTo(x+w,y,x+w,y+h,r);
-  ctx.arcTo(x+w,y+h,x,y+h,r);
-  ctx.arcTo(x,y+h,x,y,r);
-  ctx.arcTo(x,y,x+w,y,r);
-  ctx.closePath();
-}
-function clearScreen(ctx,W,H){ ctx.fillStyle=C.bg; ctx.fillRect(0,0,W,H); }
-function grid(ctx,W,H){
-  ctx.save();
-  ctx.strokeStyle='rgba(212,168,96,.08)';
-  ctx.lineWidth=1;
-  for(let x=0;x<W;x+=40){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
-  for(let y=0;y<H;y+=40){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}
-  ctx.restore();
-}
-function label(ctx,text,x,y,size,color,align='center'){
-  ctx.save();
-  ctx.font=`bold ${size}px 'Courier New',monospace`;
-  ctx.textAlign=align; ctx.textBaseline='middle';
-  ctx.shadowColor=color; ctx.shadowBlur=8;
-  ctx.fillStyle=color;
-  ctx.fillText(text,x,y);
-  ctx.restore();
-}
-function dimText(ctx,text,x,y,size,align='center'){
-  ctx.save();
-  ctx.font=`${size}px 'Courier New',monospace`;
-  ctx.textAlign=align; ctx.textBaseline='middle';
-  ctx.fillStyle='rgba(232,208,160,.5)';
-  ctx.fillText(text,x,y);
-  ctx.restore();
-}
-function waveSample(phase, type){
-  const p = phase - Math.floor(phase);
-  switch(type){
-    case 'sine':   return Math.sin(p*Math.PI*2);
-    case 'saw':    return 2*p - 1;
-    case 'square': return p < 0.5 ? 1 : -1;
-    case 'noise':  return Math.random()*2 - 1;
-    case 'voice':
-      return Math.sin(p*Math.PI*2)*0.5
-           + Math.sin(p*Math.PI*4)*0.25
-           + Math.sin(p*Math.PI*6)*0.12
-           + (Math.random()*2-1)*0.1;
-  }
-  return 0;
-}
-
-/* =========================================================
-   ОБЩЕЕ СОСТОЯНИЕ
-   ========================================================= */
-const imit = { freq:140, amp:0.6, wave:'sine', mod:0.3 };
-let finalMode = 'imitation';
-let micOn = false;
-let audioCtx=null, analyser=null, timeData=null, freqData=null, micStream=null;
-let mediaRecorder=null, recordedChunks=[], recordedUrl=null;
-const isFileProtocol = (location.protocol === 'file:');
-
-/* =========================================================
-   ОЗВУЧКА
-   ========================================================= */
-let ruVoice = null;
-let voiceOn = true;
-let currentlySpeaking = false;
-let speakToken = 0;
-let currentLabText = '';
-
-function pickVoice(){
-  if(!('speechSynthesis' in window)) return;
-  const list = speechSynthesis.getVoices();
-  if(!list.length) return;
-  ruVoice =
-    list.find(v=>/ru[-_]RU/i.test(v.lang) && /yuri|dmitri|pavel|male|мужск/i.test(v.name)) ||
-    list.find(v=>/ru[-_]RU/i.test(v.lang)) ||
-    list.find(v=>/^ru/i.test(v.lang)) || null;
-}
-if('speechSynthesis' in window){
-  pickVoice();
-  speechSynthesis.onvoiceschanged = pickVoice;
-}
-function setSpeaking(on){ currentlySpeaking = on; }
-
-function splitSentences(text){
-  const parts = text.match(/[^.!?…]+[.!?…]+/g);
-  if(!parts) return [text.trim()];
-  const rest = text.slice(parts.join('').length).trim();
-  if(rest) parts.push(rest);
-  return parts.map(s=>s.trim()).filter(Boolean);
-}
-
-function speakText(text, onDone){
-  if(!voiceOn || !('speechSynthesis' in window)){
-    setSpeaking(false);
-    if(onDone) setTimeout(onDone, 200);
-    return;
-  }
-  try{
-    speechSynthesis.cancel();
-    const myToken = ++speakToken;
-    const sentences = splitSentences(text);
-    let i = 0;
-    const step = ()=>{
-      if(myToken !== speakToken){ setSpeaking(false); return; }
-      if(i >= sentences.length){ setSpeaking(false); if(onDone) onDone(); return; }
-      if(i === 0) setSpeaking(true);
-      const u = new SpeechSynthesisUtterance(sentences[i]);
-      u.lang = 'ru-RU';
-      if(ruVoice) u.voice = ruVoice;
-      u.pitch = 0.75; u.rate = 0.9; u.volume = 1;
-      u.onend = ()=>{ i++; setTimeout(step, 180); };
-      u.onerror = ()=>{ i++; setTimeout(step, 180); };
-      speechSynthesis.speak(u);
-    };
-    step();
-  }catch(e){ setSpeaking(false); if(onDone) onDone(); }
-}
-function stopSpeech(){
-  speakToken++;
-  try{ speechSynthesis.cancel(); }catch(e){}
-  setSpeaking(false);
-}
-
-/* =========================================================
-   СИСТЕМА СЦЕН — теперь с интерактивом
-   ========================================================= */
-function makeScene(canvasId, drawFn, opts = {}){
-  const cv = document.getElementById(canvasId);
-  if(!cv) return null;
-  const ctx = cv.getContext('2d');
-  const BASE_W = 900;
-  const BASE_H = opts.height || parseInt(cv.dataset.h || 300, 10);
-  let visible = false;
-  let rafId = null;
-  let time = 0, last = 0;
-  const mouse = { x:-1, y:-1, inside:false, down:false };
-
-  function resize(){
-    const rect = cv.getBoundingClientRect();
-    const dpr = window.devicePixelRatio || 1;
-    const displayW = rect.width || BASE_W;
-    const displayH = displayW * BASE_H / BASE_W;
-    const targetW = Math.max(1, Math.round(displayW * dpr));
-    const targetH = Math.max(1, Math.round(displayH * dpr));
-    if(cv.width !== targetW || cv.height !== targetH){
-      cv.width = targetW; cv.height = targetH;
-    }
-    ctx.setTransform(targetW/BASE_W, 0, 0, targetH/BASE_H, 0, 0);
-    ctx.imageSmoothingQuality = 'high';
-    if(!visible) drawFn(ctx, BASE_W, BASE_H, 0, 0.016, mouse);
-  }
-
-  function loop(ts){
-    if(!visible) return;
-    if(!last) last = ts;
-    const dt = Math.min(.05, (ts - last)/1000);
-    last = ts;
-    time += dt;
-    try{ drawFn(ctx, BASE_W, BASE_H, time, dt, mouse); }catch(e){ console.error(e); }
-    rafId = requestAnimationFrame(loop);
-  }
-
-  // Мышь
-  cv.addEventListener('mousemove', e => {
-    const r = cv.getBoundingClientRect();
-    mouse.x = (e.clientX - r.left) * (BASE_W / r.width);
-    mouse.y = (e.clientY - r.top)  * (BASE_H / r.height);
-    mouse.inside = true;
-    if(opts.onMouseMove) opts.onMouseMove(mouse, ctx, BASE_W, BASE_H);
-  });
-  cv.addEventListener('mouseleave', ()=>{ mouse.inside = false; });
-  cv.addEventListener('mousedown', e => {
-    mouse.down = true;
-    if(opts.onMouseDown) opts.onMouseDown(mouse, ctx, BASE_W, BASE_H);
-  });
-  cv.addEventListener('click', e => {
-    if(opts.onClick) opts.onClick(mouse, ctx, BASE_W, BASE_H);
-  });
-  window.addEventListener('mouseup', ()=>{ mouse.down = false; if(opts.onMouseUp) opts.onMouseUp(mouse); });
-  // Тач
-  cv.addEventListener('touchstart', e => {
-    if(e.touches.length){
-      const r = cv.getBoundingClientRect();
-      mouse.x = (e.touches[0].clientX - r.left) * (BASE_W / r.width);
-      mouse.y = (e.touches[0].clientY - r.top)  * (BASE_H / r.height);
-      mouse.inside = true; mouse.down = true;
-      if(opts.onMouseDown) opts.onMouseDown(mouse, ctx, BASE_W, BASE_H);
-    }
-  }, {passive:true});
-  cv.addEventListener('touchmove', e => {
-    if(e.touches.length){
-      const r = cv.getBoundingClientRect();
-      mouse.x = (e.touches[0].clientX - r.left) * (BASE_W / r.width);
-      mouse.y = (e.touches[0].clientY - r.top)  * (BASE_H / r.height);
-      if(opts.onMouseMove) opts.onMouseMove(mouse, ctx, BASE_W, BASE_H);
-    }
-  }, {passive:true});
-  cv.addEventListener('touchend', e => {
-    mouse.down = false;
-    if(opts.onMouseUp) opts.onMouseUp(mouse);
-    if(opts.onClick) opts.onClick(mouse, ctx, BASE_W, BASE_H);
-  });
-
-  const observer = new IntersectionObserver(entries => {
-    for(const e of entries){
-      if(e.isIntersecting && !visible){
-        visible = true; last = 0; resize();
-        rafId = requestAnimationFrame(loop);
-      } else if(!e.isIntersecting && visible){
-        visible = false;
-        if(rafId) cancelAnimationFrame(rafId);
-      }
-    }
-  }, { threshold: 0.1 });
-  window.addEventListener('resize', resize);
-  resize();
-  observer.observe(cv);
-  return { cv, ctx, BASE_W, BASE_H, mouse, get visible(){ return visible; }, resize };
-}
-
-/* =========================================================
-   ГЛАВА 1 · ЗВУК — интерактив: слайдер громкости
-   ========================================================= */
-const soundState = { volume:0.6, dragging:false };
-const soundScene = makeScene('cv-sound', (ctx, W, H, t, dt, mouse) => {
-  clearScreen(ctx, W, H); grid(ctx, W, H);
-  const cy = H/2 - 20;
-  const vol = soundState.volume;
-  const amp = 8 + vol * 42;
-
-  // Динамик
-  ctx.save();
-  ctx.strokeStyle = C.amber; ctx.lineWidth = 3;
-  ctx.shadowColor = C.amber; ctx.shadowBlur = 12;
-  ctx.fillStyle = 'rgba(212,168,96,.07)';
-  ctx.beginPath();
-  ctx.moveTo(40,cy-60); ctx.lineTo(110,cy-60);
-  ctx.lineTo(155,cy-105); ctx.lineTo(155,cy+105);
-  ctx.lineTo(110,cy+60); ctx.lineTo(40,cy+60);
-  ctx.closePath(); ctx.fill(); ctx.stroke();
-  ctx.restore();
-
-  for(let i=0;i<5;i++){
-    const p = ((t*.9 + i*.2) % 1);
-    ctx.globalAlpha = (1-p) * (.15 + vol*.7);
-    ctx.strokeStyle = C.terra; ctx.lineWidth = 2 + vol*2.5;
-    ctx.beginPath(); ctx.arc(155, cy, 30 + p*280, -.55, .55); ctx.stroke();
-  }
-  ctx.globalAlpha = 1;
-
-  const n = 68;
-  for(let i=0;i<n;i++){
-    const baseX = 210 + i*9.6;
-    const phase = i*.55 - t*5.5;
-    const disp = amp * Math.sin(phase);
-    const x = baseX + disp;
-    const y = cy + Math.sin(i*1.7)*8;
-    const c = Math.sin(phase) > 0 ? C.gold : C.khaki;
-    ctx.fillStyle = c; ctx.shadowColor = c; ctx.shadowBlur = 4 + vol*8;
-    ctx.beginPath(); ctx.arc(x,y,2.4 + vol*2.4,0,Math.PI*2); ctx.fill();
-  }
-  ctx.shadowBlur = 0;
-
-  // Слайдер
-  const sbX = 380, sbY = H - 60, sbW = 320, sbH = 10;
-  ctx.save();
-  ctx.fillStyle = 'rgba(212,168,96,.15)';
-  ctx.fillRect(sbX, sbY, sbW, sbH);
-  ctx.fillStyle = C.amber;
-  ctx.fillRect(sbX, sbY, sbW * vol, sbH);
-  const hx = sbX + sbW * vol;
-  ctx.fillStyle = C.gold;
-  ctx.beginPath(); ctx.arc(hx, sbY + sbH/2, 10, 0, Math.PI*2); ctx.fill();
-  ctx.strokeStyle = '#1a1005'; ctx.lineWidth = 2; ctx.stroke();
-  ctx.restore();
-
-  dimText(ctx, 'громкость', sbX + sbW/2, sbY + 30, 11);
-  label(ctx, 'ЗВУК — КОЛЕБАНИЯ ДАВЛЕНИЯ ВОЗДУХА', W/2, H-110, 15, C.amber);
-  dimText(ctx, 'тяни ползунок внизу — громкость меняется', W/2, 30, 11);
-
-  // Курсор над слайдером
-  if(mouse.inside && mouse.y > sbY - 16 && mouse.y < sbY + 26){
-    ctx.canvas.style.cursor = 'ew-resize';
-    if(soundState.dragging || mouse.down){
-      soundState.volume = Math.max(0, Math.min(1, (mouse.x - sbX) / sbW));
-    }
-  } else {
-    ctx.canvas.style.cursor = 'default';
-  }
-}, {
-  height:300,
-  onMouseDown: (mouse)=>{ soundState.dragging = true; },
-  onMouseUp: ()=>{ soundState.dragging = false; }
-});
-
-/* =========================================================
-   ГЛАВА 2 · МЕМБРАНА — интерактив: клик толкает
-   ========================================================= */
-const memState = { touch:0, touchT:0 };
-const membraneScene = makeScene('cv-membrane', (ctx, W, H, t, dt, mouse) => {
-  clearScreen(ctx, W, H); grid(ctx, W, H);
-  const mx = W/2 + 40, top = 60, bot = H - 70;
-
-  memState.touchT += dt;
-  if(memState.touch > 0.001){
-    memState.touch *= 0.985;
-  } else {
-    memState.touch = 0;
-  }
-
-  const soundWave = Math.sin(t*6);
-  const userPush = memState.touch * Math.sin(memState.touchT * 18);
-  const bow = (20 + 30*Math.abs(soundWave)) * soundWave + userPush * 40;
-
-  for(let i=0;i<6;i++){
-    const p = ((t*.9 + i*.17) % 1);
-    const x = mx - 50 - p*280;
-    ctx.globalAlpha = (1-p)*.45;
-    ctx.strokeStyle = C.terra; ctx.lineWidth = 3;
-    ctx.beginPath(); ctx.moveTo(x, top+10); ctx.lineTo(x, bot-10); ctx.stroke();
-  }
-  ctx.globalAlpha = 1;
-
-  ctx.save();
-  ctx.strokeStyle = C.amber; ctx.lineWidth = 5;
-  ctx.shadowColor = C.amber; ctx.shadowBlur = 16;
-  ctx.beginPath();
-  const SEG = 44;
-  for(let i=0;i<=SEG;i++){
-    const p = i/SEG;
-    const y = top + (bot-top)*p;
-    const shape = Math.sin(Math.PI*p);
-    const x = mx + bow*shape;
-    if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
-  }
-  ctx.stroke();
-  ctx.restore();
-
-  ctx.strokeStyle = 'rgba(168,120,138,.55)'; ctx.lineWidth = 3;
-  ctx.beginPath(); ctx.moveTo(mx,top); ctx.lineTo(mx-70,top-30); ctx.stroke();
-  ctx.beginPath(); ctx.moveTo(mx,bot); ctx.lineTo(mx-70,bot+30); ctx.stroke();
-  ctx.beginPath(); ctx.moveTo(mx,top); ctx.lineTo(mx+70,top-30); ctx.stroke();
-  ctx.beginPath(); ctx.moveTo(mx,bot); ctx.lineTo(mx+70,bot+30); ctx.stroke();
-
-  label(ctx,'МЕМБРАНА', mx, 30, 18, C.amber);
-  label(ctx,'повторяет колебания воздуха', W/2, H-30, 14, C.khaki);
-  dimText(ctx,'клик по мембране — толчок', W/2, 30, 11);
-
-  if(mouse.inside && Math.abs(mouse.x - mx) < 100 && mouse.y > top - 20 && mouse.y < bot + 20){
-    ctx.canvas.style.cursor = 'pointer';
-  } else {
-    ctx.canvas.style.cursor = 'default';
-  }
-}, {
-  height:320,
-  onMouseDown: (mouse)=>{
-    const mx = 900/2 + 40;
-    if(Math.abs(mouse.x - mx) < 100){
-      memState.touch = 1;
-      memState.touchT = 0;
-    }
-  }
-});
-
-/* =========================================================
-   ГЛАВА 3 · КАТУШКА — интерактив: тащи катушку
-   ========================================================= */
-const coilState = { x:0, v:0, dragging:false, dragOffset:0 };
-const coilScene = makeScene('cv-coil', (ctx, W, H, t, dt, mouse) => {
-  clearScreen(ctx, W, H); grid(ctx, W, H);
-  const cy = H/2 - 20;
-
-  // Физика
-  if(coilState.dragging){
-    // Следуем за мышью
-    const targetX = Math.max(-80, Math.min(80, mouse.x - W/2 - coilState.dragOffset));
-    coilState.x += (targetX - coilState.x) * Math.min(1, dt * 16);
-    coilState.v = (targetX - coilState.x) * 0.2;
-  } else {
-    // Автоколебание
-    if(Math.abs(coilState.v) < 0.01 && Math.abs(coilState.x) < 0.5){
-      coilState.x = Math.sin(t*2.2) * 60;
-      coilState.v = Math.cos(t*2.2) * 60 * 2.2;
-    } else {
-      const k = 25, damp = 1.8;
-      coilState.v += (-k*coilState.x - damp*coilState.v) * dt;
-      coilState.x += coilState.v * dt;
-    }
-  }
-  const cur = Math.max(-1, Math.min(1, coilState.v * 0.08));
-
-  // N
-  ctx.save();
-  ctx.fillStyle='rgba(184,104,72,.16)'; ctx.strokeStyle=C.rust; ctx.lineWidth=3;
-  ctx.shadowColor=C.rust; ctx.shadowBlur=12;
-  ctx.fillRect(150,cy-100,65,200); ctx.strokeRect(150,cy-100,65,200);
-  ctx.restore();
-  label(ctx,'N',182,cy,42,C.rust);
-
-  // S
-  ctx.save();
-  ctx.fillStyle='rgba(138,152,96,.16)'; ctx.strokeStyle=C.olive; ctx.lineWidth=3;
-  ctx.shadowColor=C.olive; ctx.shadowBlur=12;
-  ctx.fillRect(W-215,cy-100,65,200); ctx.strokeRect(W-215,cy-100,65,200);
-  ctx.restore();
-  label(ctx,'S',W-182,cy,42,C.olive);
-
-  // Силовые линии
-  ctx.save();
-  ctx.strokeStyle='rgba(212,168,96,.22)'; ctx.lineWidth=1.5;
-  for(let i=0;i<8;i++){
-    const y = cy-88+i*25;
-    ctx.beginPath(); ctx.moveTo(215,y); ctx.lineTo(W-215,y); ctx.stroke();
-  }
-  ctx.restore();
-
-  // Катушка
-  const cx = W/2 + coilState.x;
-  ctx.save();
-  ctx.strokeStyle = C.gold; ctx.lineWidth = 3;
-  ctx.shadowColor = C.gold; ctx.shadowBlur = coilState.dragging ? 20 : 12;
-  for(let i=0;i<7;i++){
-    const lx = cx - 55 + i*(110/6);
-    ctx.beginPath(); ctx.ellipse(lx, cy, 8, 72, 0, 0, Math.PI*2); ctx.stroke();
-  }
-  ctx.restore();
-
-  // Гальванометр
-  const gx = W - 130, gy = cy + 130;
-  ctx.save();
-  ctx.fillStyle='#1a1005'; ctx.strokeStyle='rgba(232,208,160,.3)'; ctx.lineWidth=2;
-  ctx.beginPath(); ctx.arc(gx,gy,38,0,Math.PI*2); ctx.fill(); ctx.stroke();
-  const needle = cur * 1.15;
-  ctx.strokeStyle = Math.abs(cur) > .02 ? C.khaki : '#5a5040';
-  ctx.shadowColor = C.khaki; ctx.shadowBlur = Math.abs(cur) * 16;
-  ctx.lineWidth = 3;
-  ctx.beginPath();
-  ctx.moveTo(gx, gy + 22);
-  ctx.lineTo(gx + Math.sin(needle)*32, gy + 22 - Math.cos(needle)*40);
-  ctx.stroke();
-  ctx.restore();
-
-  label(ctx,'КАТУШКА В МАГНИТНОМ ПОЛЕ', W/2, 30, 18, C.amber);
-  label(ctx,'возникает ток', W/2, H-30, 14, C.khaki);
-  dimText(ctx,'тяни катушку мышью — отпусти, чтобы качнулась', W/2, 52, 11);
-
-  // Курсор над катушкой
-  if(mouse.inside && Math.abs(mouse.x - cx) < 90 && Math.abs(mouse.y - cy) < 100){
-    ctx.canvas.style.cursor = coilState.dragging ? 'grabbing' : 'grab';
-  } else {
-    ctx.canvas.style.cursor = 'default';
-  }
-}, {
-  height:360,
-  onMouseDown: (mouse)=>{
-    const cx = 900/2 + coilState.x;
-    if(Math.abs(mouse.x - cx) < 90 && Math.abs(mouse.y - (360/2 - 20)) < 100){
-      coilState.dragging = true;
-      coilState.dragOffset = mouse.x - (900/2 + coilState.x);
-    }
-  },
-  onMouseUp: ()=>{ coilState.dragging = false; }
-});
-
-/* =========================================================
-   ГЛАВА 4 · ФАРАДЕЙ — интерактив: кнопка «Магнит»
-   ========================================================= */
-const faradayState = { magnetOn:true };
-const faradayScene = makeScene('cv-faraday', (ctx, W, H, t, dt, mouse) => {
-  clearScreen(ctx, W, H); grid(ctx, W, H);
-  const cy = H*0.32;
-  const magnetOn = faradayState.magnetOn;
-  const coilX = magnetOn
-    ? Math.sin(t*2.2) * 60
-    : (Math.sin(t*2.2) * 60 * 0.15);
-
-  // N
-  ctx.save();
-  ctx.fillStyle = magnetOn ? 'rgba(184,104,72,.14)' : 'rgba(184,104,72,.05)';
-  ctx.strokeStyle = magnetOn ? C.rust : 'rgba(184,104,72,.3)';
-  ctx.lineWidth=3;
-  ctx.shadowColor=C.rust; ctx.shadowBlur = magnetOn ? 10 : 0;
-  ctx.fillRect(160,cy-60,50,120); ctx.strokeRect(160,cy-60,50,120);
-  ctx.restore();
-  label(ctx,'N',185,cy,26, magnetOn ? C.rust : 'rgba(184,104,72,.4)');
-
-  // S
-  ctx.save();
-  ctx.fillStyle = magnetOn ? 'rgba(138,152,96,.14)' : 'rgba(138,152,96,.05)';
-  ctx.strokeStyle = magnetOn ? C.olive : 'rgba(138,152,96,.3)';
-  ctx.lineWidth=3;
-  ctx.shadowColor=C.olive; ctx.shadowBlur = magnetOn ? 10 : 0;
-  ctx.fillRect(W-210,cy-60,50,120); ctx.strokeRect(W-210,cy-60,50,120);
-  ctx.restore();
-  label(ctx,'S',W-185,cy,26, magnetOn ? C.olive : 'rgba(138,152,96,.4)');
-
-  // Катушка
-  const cx = W/2 + coilX;
-  ctx.save();
-  ctx.strokeStyle=C.gold; ctx.lineWidth=3;
-  ctx.shadowColor=C.gold; ctx.shadowBlur=10;
-  for(let i=0;i<6;i++){
-    const lx = cx - 45 + i*(90/5);
-    ctx.beginPath(); ctx.ellipse(lx, cy, 7, 46, 0, 0, Math.PI*2); ctx.stroke();
-  }
-  ctx.restore();
-
-  // График ЭДС
-  const gTop = H*.62, gBot = H - 40, gMid = (gTop+gBot)/2;
-  ctx.save();
-  ctx.strokeStyle='rgba(168,120,138,.28)'; ctx.lineWidth=1.5;
-  ctx.strokeRect(80, gTop, W-160, gBot-gTop);
-  ctx.beginPath(); ctx.moveTo(80,gMid); ctx.lineTo(W-80,gMid); ctx.stroke();
-  ctx.restore();
-
-  ctx.save();
-  ctx.strokeStyle = magnetOn ? C.khaki : 'rgba(184,200,120,.25)';
-  ctx.lineWidth = 2.4;
-  ctx.shadowColor = C.khaki; ctx.shadowBlur = magnetOn ? 10 : 0;
-  ctx.beginPath();
-  for(let x=0;x<=W-160;x++){
-    const u = x/(W-160);
-    const amp = magnetOn ? 40 : 4;
-    const y = gMid - Math.cos((t*2.2 + u*3.6*Math.PI))*amp;
-    if(x===0) ctx.moveTo(80+x, y); else ctx.lineTo(80+x, y);
-  }
-  ctx.stroke();
-  ctx.restore();
-
-  dimText(ctx,'ЭДС индукции', 92, gTop+16, 11, 'left');
-  dimText(ctx,'время →', W-92, gBot-8, 11, 'right');
-  label(ctx,'ε = − dΦ / dt', W-150, gTop+20, 18, C.terra);
-  label(ctx,'ЗАКОН ЭЛЕКТРОМАГНИТНОЙ ИНДУКЦИИ', W/2, 30, 18, C.amber);
-
-  // Кнопка
-  const btnW = 180, btnH = 36;
-  const bx = W/2 - btnW/2, by = H - 66;
-  const hover = mouse.inside && mouse.x > bx && mouse.x < bx+btnW && mouse.y > by && mouse.y < by+btnH;
-  ctx.save();
-  ctx.fillStyle = magnetOn ? C.rust : 'rgba(184,104,72,.4)';
-  if(hover) ctx.fillStyle = magnetOn ? '#d88858' : 'rgba(184,104,72,.55)';
-  rr(ctx, bx, by, btnW, btnH, 4);
-  ctx.fill();
-  ctx.strokeStyle = '#1a1005'; ctx.lineWidth = 2; ctx.stroke();
-  ctx.fillStyle = '#f0f8e0';
-  ctx.font = "bold 13px 'Courier New',monospace";
-  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
-  ctx.fillText(magnetOn ? 'МАГНИТ: ВКЛ' : 'МАГНИТ: ВЫКЛ', W/2, by + btnH/2);
-  ctx.restore();
-
-  ctx.canvas.style.cursor = hover ? 'pointer' : 'default';
-}, {
-  height:340,
-  onMouseDown: (mouse)=>{
-    const btnW = 180, btnH = 36;
-    const bx = 900/2 - btnW/2, by = 340 - 66;
-    if(mouse.x > bx && mouse.x < bx+btnW && mouse.y > by && mouse.y < by+btnH){
-      faradayState.magnetOn = !faradayState.magnetOn;
-    }
-  }
-});
-
-/* =========================================================
-   ГЛАВА 5 · СИГНАЛ — интерактив: усиление + шум
-   ========================================================= */
-const signalState = { gain:0.5, noise:false, noiseMix:0 };
-const signalScene = makeScene('cv-signal', (ctx, W, H, t, dt, mouse) => {
-  clearScreen(ctx, W, H); grid(ctx, W, H);
-  const cy = H/2 - 10;
-
-  ctx.save();
-  ctx.strokeStyle='rgba(212,168,96,.35)'; ctx.lineWidth=1.5;
-  ctx.beginPath(); ctx.moveTo(50,cy); ctx.lineTo(W-50,cy); ctx.stroke();
-  ctx.restore();
-
-  // Плавное появление шума
-  const targetMix = signalState.noise ? 1 : 0;
-  signalState.noiseMix += (targetMix - signalState.noiseMix) * 0.1;
-
-  const gain = 20 + signalState.gain * 100;
-
-  ctx.save();
-  ctx.strokeStyle = C.khaki; ctx.lineWidth = 2.4;
-  ctx.shadowColor = C.khaki; ctx.shadowBlur = 12;
-  ctx.beginPath();
-  for(let x=50;x<=W-50;x++){
-    const u = (x-50)/(W-100);
-    const env = Math.exp(-Math.pow((u-.5)*2.4, 2));
-    let v = env*(
-      Math.sin(u*32 - t*9)*.55 +
-      Math.sin(u*74 - t*14)*.28 +
-      Math.sin(u*14 - t*5)*.45);
-    v += (Math.random()-0.5) * 0.8 * signalState.noiseMix;
-    const y = cy - gain * v;
-    if(x===50) ctx.moveTo(x,y); else ctx.lineTo(x,y);
-  }
-  ctx.stroke();
-  ctx.restore();
-
-  // Заголовок
-  label(ctx,'ЭЛЕКТРИЧЕСКИЙ СИГНАЛ', W/2, 30, 18, C.amber);
-  dimText(ctx, '~ милливольты · точная копия голоса', W/2, 52, 11);
-
-  // Ползунок усиления
-  const sbX = 100, sbY = H - 55, sbW = 280, sbH = 8;
-  ctx.save();
-  ctx.fillStyle = 'rgba(212,168,96,.15)';
-  ctx.fillRect(sbX, sbY, sbW, sbH);
-  ctx.fillStyle = C.khaki;
-  ctx.fillRect(sbX, sbY, sbW * signalState.gain, sbH);
-  const hx = sbX + sbW * signalState.gain;
-  ctx.fillStyle = C.gold;
-  ctx.beginPath(); ctx.arc(hx, sbY + sbH/2, 9, 0, Math.PI*2); ctx.fill();
-  ctx.strokeStyle = '#1a1005'; ctx.lineWidth = 2; ctx.stroke();
-  ctx.restore();
-  dimText(ctx, 'усиление', sbX + sbW/2, sbY + 26, 11);
-
-  // Кнопка шума
-  const nbtnW = 100, nbtnH = 32;
-  const nbx = W - 200, nby = H - 66;
-  const hoverN = mouse.inside && mouse.x > nbx && mouse.x < nbx+nbtnW && mouse.y > nby && mouse.y < nby+nbtnH;
-  ctx.save();
-  ctx.fillStyle = signalState.noise ? C.terra : 'rgba(212,168,96,.15)';
-  if(hoverN) ctx.fillStyle = signalState.noise ? '#d88858' : 'rgba(212,168,96,.28)';
-  rr(ctx, nbx, nby, nbtnW, nbtnH, 4);
-  ctx.fill();
-  ctx.strokeStyle = '#1a1005'; ctx.lineWidth = 2; ctx.stroke();
-  ctx.fillStyle = signalState.noise ? '#f0f8e0' : C.amber;
-  ctx.font = "bold 12px 'Courier New',monospace";
-  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
-  ctx.fillText('ШУМ: ' + (signalState.noise ? 'ВКЛ' : 'ВЫКЛ'), nbx + nbtnW/2, nby + nbtnH/2);
-  ctx.restore();
-
-  // Курсор
-  const overSlider = mouse.inside && mouse.y > sbY - 14 && mouse.y < sbY + 20;
-  if(overSlider){
-    ctx.canvas.style.cursor = 'ew-resize';
-    if(mouse.down){
-      signalState.gain = Math.max(0, Math.min(1, (mouse.x - sbX) / sbW));
-    }
-  } else if(hoverN){
-    ctx.canvas.style.cursor = 'pointer';
-  } else {
-    ctx.canvas.style.cursor = 'default';
-  }
-}, {
-  height:280,
-  onMouseDown: (mouse)=>{
-    const nbtnW = 100, nbtnH = 32;
-    const nbx = 900 - 200, nby = 280 - 66;
-    if(mouse.x > nbx && mouse.x < nbx+nbtnW && mouse.y > nby && mouse.y < nby+nbtnH){
-      signalState.noise = !signalState.noise;
-    }
-  }
-});
-
-/* =========================================================
-   ГЛАВА 6 · ЛАБОРАТОРИЯ
-   ========================================================= */
-const SPEC_BARS = 96;
-const SPEC_H = 70;
-let spectroCanvas = document.createElement('canvas');
-let spectroCtx = spectroCanvas.getContext('2d');
-let spectroW = 780;
-spectroCanvas.width = spectroW;
-spectroCanvas.height = SPEC_H;
-spectroCtx.fillStyle = '#0a0603';
-spectroCtx.fillRect(0,0,spectroW,SPEC_H);
-let spectroAccum = 0;
-const SPECTRO_INTERVAL = 1/30;
-
-function spectroColor(v){
-  v = Math.max(0, Math.min(1, v));
-  const stops = [
-    [0.00,[10,6,3]], [0.10,[26,10,5]], [0.25,[74,32,16]],
-    [0.45,[138,64,32]], [0.65,[200,120,80]], [0.80,[212,168,96]],
-    [0.92,[240,216,144]], [1.00,[255,248,224]]
-  ];
-  for(let i=1;i<stops.length;i++){
-    if(v <= stops[i][0]){
-      const t = (v - stops[i-1][0]) / (stops[i][0] - stops[i-1][0]);
-      const a = stops[i-1][1], b = stops[i][1];
-      return `rgb(${Math.round(a[0]+(b[0]-a[0])*t)},${Math.round(a[1]+(b[1]-a[1])*t)},${Math.round(a[2]+(b[2]-a[2])*t)})`;
-    }
-  }
-  return 'rgb(255,248,224)';
-}
-function updateSpectrogram(barValues){
-  const img = spectroCtx.getImageData(0, 0, spectroW, SPEC_H - 1);
-  spectroCtx.putImageData(img, 0, 1);
-  const barW = spectroW / barValues.length;
-  for(let i=0;i<barValues.length;i++){
-    spectroCtx.fillStyle = spectroColor(barValues[i]);
-    spectroCtx.fillRect(Math.floor(i*barW), 0, Math.ceil(barW)+1, 1);
-  }
-}
-
-let wavePhase = 0;
-const labScene = makeScene('cv-lab', (ctx, W, H, t, dt) => {
-  clearScreen(ctx, W, H); grid(ctx, W, H);
-  const cy = H*0.28;
-  const specTop = H - 100;
-  const specX = 60, specW = W - 120;
-
-  ctx.save();
-  ctx.strokeStyle='rgba(212,168,96,.28)'; ctx.lineWidth=2;
-  ctx.strokeRect(60, 50, W-120, specTop - 70);
-  ctx.restore();
-
-  const spanX = W - 120;
-  const scaleY = (specTop - 90) * 0.42;
-
-  ctx.save();
-  ctx.lineWidth = 2.2; ctx.lineJoin = 'round'; ctx.lineCap = 'round';
-
-  if(finalMode === 'mic' && micOn && analyser){
-    ctx.strokeStyle = C.khaki; ctx.shadowColor = C.khaki; ctx.shadowBlur = 14;
-    analyser.getByteTimeDomainData(timeData);
-    const n = timeData.length;
-    ctx.beginPath();
-    for(let i=0;i<n;i++){
-      const x = 60 + (i/(n-1))*spanX;
-      const y = cy - ((timeData[i]-128)/128) * scaleY * 1.5;
-      if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
-    }
-    ctx.stroke();
-  }
-  else if(finalMode === 'imitation'){
-    ctx.strokeStyle = C.amber; ctx.shadowColor = C.amber; ctx.shadowBlur = 14;
-    wavePhase += dt * (0.6 + imit.freq / 90);
-    const cycles = 3 + (imit.freq / 200);
-    ctx.beginPath();
-    for(let px=0; px<=spanX; px++){
-      const u = px / spanX;
-      const phase = u*cycles + wavePhase;
-      let v = waveSample(phase, imit.wave);
-      const env = 1 - imit.mod*0.55 + imit.mod*0.55 * (0.5 + 0.5*Math.sin(u*Math.PI*4 - t*2.4));
-      v *= imit.amp * env;
-      const y = cy - v * scaleY;
-      const x = 60 + px;
-      if(px===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
-    }
-    ctx.stroke();
-  }
-  else if(finalMode === 'text' && currentlySpeaking){
-    ctx.strokeStyle = C.gold; ctx.shadowColor = C.gold; ctx.shadowBlur = 14;
-    wavePhase += dt * 22;
-    const cycles = 10;
-    ctx.beginPath();
-    for(let px=0; px<=spanX; px++){
-      const u = px / spanX;
-      const phase = u*cycles + wavePhase*0.18;
-      let v = waveSample(phase, 'voice');
-      const syllable = 0.45 + 0.55*Math.abs(Math.sin(t*7 + u*2));
-      const wordEnv  = 0.7 + 0.3*Math.sin(t*1.4);
-      v *= syllable * wordEnv * 0.8;
-      const y = cy - v * scaleY;
-      const x = 60 + px;
-      if(px===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
-    }
-    ctx.stroke();
-  }
-  else {
-    ctx.strokeStyle = 'rgba(212,168,96,.4)';
-    ctx.shadowColor = C.amber; ctx.shadowBlur = 8;
-    ctx.beginPath();
-    for(let px=0; px<=spanX; px++){
-      const u = px / spanX;
-      const v = 0.012*Math.sin(u*40 + t*4) + (Math.random()-0.5)*0.012;
-      const y = cy - v * scaleY;
-      const x = 60 + px;
-      if(px===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
-    }
-    ctx.stroke();
-  }
-  ctx.restore();
-
-  // Заголовки
-  let title = '', sub = '';
-  if(finalMode === 'imitation'){
-    title = 'ГЕНЕРАТОР СИГНАЛОВ';
-    sub = 'ползунки меняют волну в реальном времени';
-  } else if(finalMode === 'mic'){
-    title = micOn ? 'ЖИВОЙ СИГНАЛ С МИКРОФОНА' : 'МИКРОФОН НЕ АКТИВЕН';
-    sub = micOn ? 'говорите — или нажмите «ЗАПИСАТЬ»' : 'нажмите вкладку «Микрофон»';
-  } else {
-    title = currentlySpeaking ? 'ОЗВУЧИВАНИЕ ТЕКСТА' : 'РЕЖИМ: ТЕКСТ';
-    sub = currentlySpeaking ? 'волна идёт синхронно с речью' : 'напишите фразу ниже и нажмите Enter';
-  }
-  label(ctx, title, W/2, 28, 16, C.amber);
-  dimText(ctx, sub, W/2, 46, 11);
-
-  // Отображение введённого текста в режиме «Текст»
-  if(finalMode === 'text' && currentLabText){
-    const txt = currentLabText.length > 70
-      ? currentLabText.slice(0, 67) + '…'
-      : currentLabText;
-    ctx.save();
-    ctx.font = "italic 16px 'Courier New',monospace";
-    ctx.textAlign = 'center';
-    ctx.textBaseline = 'middle';
-    ctx.fillStyle = currentlySpeaking ? C.gold : 'rgba(232,208,160,.75)';
-    ctx.shadowColor = currentlySpeaking ? C.gold : 'transparent';
-    ctx.shadowBlur = currentlySpeaking ? 14 : 0;
-    ctx.fillText('«' + txt + '»', W/2, cy);
-    ctx.restore();
-  }
-
-  // Спектрограмма
-  if(finalMode === 'mic' && micOn && analyser) analyser.getByteFrequencyData(freqData);
-
-  const barValues = new Array(SPEC_BARS);
-  for(let i=0;i<SPEC_BARS;i++){
-    let v = 0;
-    if(finalMode === 'mic' && micOn && analyser && freqData){
-      v = freqData[Math.floor((i/SPEC_BARS)*(freqData.length*0.6))] / 255;
-    } else if(finalMode === 'imitation'){
-      const center = (imit.freq/400) * SPEC_BARS;
-      const d = Math.abs(i - center);
-      v = Math.exp(-d*d/30) * imit.amp * (0.85 + 0.15*Math.sin(t*8 + i*0.3)) + Math.random()*0.08;
-      if(imit.wave === 'noise') v = (0.35 + 0.25*Math.random()) * imit.amp;
-    } else if(finalMode === 'text' && currentlySpeaking){
-      v = Math.abs(Math.sin(i*0.28 + t*9)) * 0.55 + Math.random()*0.2;
-    } else {
-      v = Math.random()*0.03;
-    }
-    v = Math.max(v, 0.015 + Math.random()*0.02);
-    barValues[i] = Math.max(0, Math.min(1, v));
-  }
-
-  spectroAccum += dt;
-  if(spectroAccum >= SPECTRO_INTERVAL){
-    spectroAccum = 0;
-    updateSpectrogram(barValues);
-  }
-
-  ctx.save();
-  ctx.imageSmoothingEnabled = false;
-  ctx.drawImage(spectroCanvas, specX, specTop, specW, SPEC_H);
-  ctx.restore();
-
-  ctx.save();
-  ctx.strokeStyle = 'rgba(212,168,96,.45)'; ctx.lineWidth = 1;
-  ctx.strokeRect(specX, specTop, specW, SPEC_H);
-  ctx.restore();
-
-  dimText(ctx,'спектрограмма · X — частота · Y — время ↓', W/2, specTop + SPEC_H + 12, 10);
-}, {height:420});
-
-/* =========================================================
-   ПЕРЕКЛЮЧЕНИЕ РЕЖИМОВ ЛАБОРАТОРИИ
-   ========================================================= */
-document.querySelectorAll('.lab-tab').forEach(tab=>{
-  tab.onclick = ()=>{
-    document.querySelectorAll('.lab-tab').forEach(t=>t.classList.remove('active'));
-    tab.classList.add('active');
-    finalMode = tab.dataset.mode;
-
-    document.getElementById('panel-imitation').classList.toggle('on', finalMode==='imitation');
-    document.getElementById('panel-mic').classList.toggle('on', finalMode==='mic');
-    document.getElementById('panel-text').classList.toggle('on', finalMode==='text');
-
-    if(finalMode !== 'mic'){ disableMic(); stopRecording(); }
-    if(finalMode === 'mic'){ enableMic(); }
-    if(finalMode === 'text'){
-      setTimeout(()=>document.getElementById('textInput').focus(), 80);
-    }
-    if(finalMode !== 'text'){
-      stopSpeech();
-      currentLabText = '';
-    }
-  };
-});
-
-/* =========================================================
-   МИКРОФОН
-   ========================================================= */
-async function enableMic(){
-  if(micOn) return;
-  if(isFileProtocol){
-    alert('Микрофон недоступен со страницы-файла. Используйте localhost или https.');
-    document.querySelector('.lab-tab[data-mode="imitation"]').click();
-    return;
-  }
-  if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){
-    alert('Микрофон не поддерживается');
-    document.querySelector('.lab-tab[data-mode="imitation"]').click();
-    return;
-  }
-  try{
-    micStream = await navigator.mediaDevices.getUserMedia({ audio:true });
-    audioCtx = audioCtx || new (window.AudioContext||window.webkitAudioContext)();
-    if(audioCtx.state === 'suspended') await audioCtx.resume();
-    const src = audioCtx.createMediaStreamSource(micStream);
-    analyser = audioCtx.createAnalyser();
-    analyser.fftSize = 2048;
-    analyser.smoothingTimeConstant = .55;
-    src.connect(analyser);
-    timeData = new Uint8Array(analyser.fftSize);
-    freqData = new Uint8Array(analyser.frequencyBinCount);
-    micOn = true;
-  }catch(e){
-    alert('Микрофон недоступен: ' + e.message);
-    document.querySelector('.lab-tab[data-mode="imitation"]').click();
-  }
-}
-function disableMic(){
-  if(micStream) micStream.getTracks().forEach(x=>x.stop());
-  micStream = null; analyser = null; micOn = false;
-  document.getElementById('levelVal').textContent = '0%';
-}
-function startRecording(){
-  if(!micStream) return;
-  try{
-    recordedChunks = [];
-    const mime = MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' :
-                 MediaRecorder.isTypeSupported('audio/mp4')  ? 'audio/mp4' : '';
-    mediaRecorder = new MediaRecorder(micStream, mime ? { mimeType:mime } : undefined);
-    mediaRecorder.ondataavailable = e => { if(e.data.size) recordedChunks.push(e.data); };
-    mediaRecorder.onstop = () => {
-      const blob = new Blob(recordedChunks, { type: recordedChunks[0]?.type || 'audio/webm' });
-      if(recordedUrl) URL.revokeObjectURL(recordedUrl);
-      recordedUrl = URL.createObjectURL(blob);
-      document.getElementById('playBtn').disabled = false;
-    };
-    mediaRecorder.start();
-    document.getElementById('recBtn').classList.add('on');
-    document.getElementById('recBtn').textContent = '■ СТОП';
-  }catch(e){}
-}
-function stopRecording(){
-  if(mediaRecorder && mediaRecorder.state !== 'inactive'){
-    mediaRecorder.stop();
-    document.getElementById('recBtn').classList.remove('on');
-    document.getElementById('recBtn').textContent = '● ЗАПИСАТЬ';
-  }
-}
-
-document.getElementById('recBtn').onclick = ()=>{
-  if(mediaRecorder && mediaRecorder.state === 'recording') stopRecording();
-  else startRecording();
-};
-document.getElementById('playBtn').onclick = ()=>{
-  if(!recordedUrl) return;
-  new Audio(recordedUrl).play();
-};
-
-/* =========================================================
-   ПОЛЗУНКИ ГЕНЕРАТОРА
-   ========================================================= */
-const freqRange = document.getElementById('freqRange');
-const ampRange  = document.getElementById('ampRange');
-const modRange  = document.getElementById('modRange');
-const waveSelect= document.getElementById('waveSelect');
-const freqVal   = document.getElementById('freqVal');
-const ampVal    = document.getElementById('ampVal');
-const modVal    = document.getElementById('modVal');
-
-freqRange.oninput = ()=>{ imit.freq = +freqRange.value; freqVal.textContent = imit.freq + ' Гц'; };
-ampRange.oninput  = ()=>{ imit.amp  = +ampRange.value/100; ampVal.textContent = ampRange.value + '%'; };
-modRange.oninput  = ()=>{ imit.mod  = +modRange.value/100; modVal.textContent = modRange.value + '%'; };
-waveSelect.onchange = ()=>{ imit.wave = waveSelect.value; };
-
-/* =========================================================
-   ОЗВУЧИВАНИЕ ТЕКСТА В ЛАБОРАТОРИИ
-   ========================================================= */
-const textInput = document.getElementById('textInput');
-const speakInputBtn = document.getElementById('speakInputBtn');
-
-function speakUserPhrase(){
-  const txt = textInput.value.trim();
-  if(!txt) return;
-  stopSpeech();
-  currentLabText = txt;
-  // Небольшая задержка, чтобы волна успела «поймать» речь
-  speakText(txt, ()=>{ /* done */ });
-}
-speakInputBtn.onclick = speakUserPhrase;
-textInput.addEventListener('keydown', e=>{
-  if(e.key === 'Enter'){ e.preventDefault(); speakUserPhrase(); }
-});
-document.querySelectorAll('.chip').forEach(ch=>{
-  ch.onclick = ()=>{ textInput.value = ch.dataset.phrase; speakUserPhrase(); };
-});
-
-/* =========================================================
-   КНОПКИ В ШАПКЕ
-   ========================================================= */
-const voiceToggle = document.getElementById('voiceToggle');
-voiceToggle.onclick = ()=>{
-  voiceOn = !voiceOn;
-  voiceToggle.textContent = voiceOn ? '🔊 ГОЛОС' : '🔇 ВЫКЛ';
-  voiceToggle.classList.toggle('active', voiceOn);
-  if(!voiceOn) stopSpeech();
-};
-
-const stopBtn = document.getElementById('stopBtn');
-stopBtn.onclick = ()=>{ stopSpeech(); currentLabText = ''; };
-
-/* Озвучка текущей главы */
-const readSceneBtn = document.getElementById('readSceneBtn');
-function getCurrentScene(){
-  const scenes = document.querySelectorAll('.hero, .scene');
-  const marker = window.scrollY + window.innerHeight * 0.4;
-  let current = scenes[0];
-  for(const s of scenes){
-    if(s.offsetTop <= marker) current = s;
-  }
-  return current;
-}
-readSceneBtn.onclick = ()=>{
-  const scene = getCurrentScene();
-  if(!scene) return;
-  const parts = [];
-  const title = scene.querySelector('h1, .scene-title');
-  if(title) parts.push(title.textContent.trim());
-  scene.querySelectorAll('.scene-text p, .hero .howto').forEach(p=>{
-    parts.push(p.textContent.trim());
-  });
-  const text = parts.join(' ');
-  if(!text) return;
-  stopSpeech();
-  speakText(text);
-};
-
-/* =========================================================
-   ПРОГРЕСС + SCROLL
-   ========================================================= */
-const scrollProgress = document.getElementById('scrollProgress');
-const scrollTopBtn = document.getElementById('scrollTopBtn');
-window.addEventListener('scroll', ()=>{
-  const h = document.documentElement;
-  const scrolled = h.scrollTop / (h.scrollHeight - h.clientHeight);
-  scrollProgress.style.width = (scrolled * 100) + '%';
-}, { passive:true });
-scrollTopBtn.onclick = ()=>window.scrollTo({top:0, behavior:'smooth'});
-
-/* Появление секций */
-const sectionObserver = new IntersectionObserver(entries => {
-  for(const e of entries){
-    if(e.isIntersecting) e.target.classList.add('in-view');
-  }
-}, { threshold: 0.15 });
-document.querySelectorAll('.scene').forEach(s => sectionObserver.observe(s));
-document.getElementById('hero').classList.add('in-view');
-
-/* =========================================================
-   СТАРТ
-   ========================================================= */
-window.addEventListener('load', ()=>{
-  pickVoice();
-  [soundScene, membraneScene, coilScene, faradayScene, signalScene, labScene]
-    .forEach(s => s && s.resize());
-});
-</script>
-</body>
-</html>