project0_67.html 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892
  1. <!DOCTYPE html>
  2. <html lang="ru">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Механические волны — неон</title>
  7. <style>
  8. :root{
  9. --neon-pink:#ff2ec4;
  10. --neon-cyan:#00e5ff;
  11. --neon-purple:#a259ff;
  12. --neon-green:#39ff14;
  13. --text:#e8e6ff;
  14. }
  15. *{box-sizing:border-box;margin:0;padding:0;}
  16. html,body{height:100%;}
  17. body{
  18. background:radial-gradient(circle at 20% 20%, #1a0033 0%, #0a0014 60%, #000 100%);
  19. color:var(--text);
  20. font-family:'Segoe UI','Trebuchet MS',sans-serif;
  21. overflow:hidden;
  22. }
  23. #app{position:relative;width:100%;height:100vh;overflow:hidden;}
  24. .scene{
  25. position:absolute;inset:0;display:none;flex-direction:column;
  26. align-items:center;justify-content:center;padding:80px 24px 24px;
  27. animation:fadeIn .6s ease;overflow-y:auto;
  28. }
  29. .scene.active{display:flex;}
  30. @keyframes fadeIn{from{opacity:0;transform:translateY(20px);}to{opacity:1;transform:none;}}
  31. h1,h2{font-weight:800;letter-spacing:2px;text-transform:uppercase;text-align:center;}
  32. h1{font-size:clamp(1.8rem,5vw,3.5rem);color:#fff;
  33. text-shadow:0 0 10px var(--neon-pink),0 0 30px var(--neon-pink),0 0 60px var(--neon-purple);
  34. margin-bottom:20px;}
  35. h2{font-size:clamp(1.3rem,3.5vw,2rem);color:#fff;
  36. text-shadow:0 0 8px var(--neon-cyan),0 0 20px var(--neon-cyan);margin-bottom:16px;}
  37. p{max-width:720px;text-align:center;line-height:1.6;opacity:.9;}
  38. .btn{
  39. background:transparent;color:var(--neon-cyan);
  40. border:2px solid var(--neon-cyan);padding:12px 28px;margin:8px;
  41. font-size:1rem;font-weight:700;letter-spacing:2px;text-transform:uppercase;
  42. border-radius:8px;cursor:pointer;transition:.25s;
  43. box-shadow:0 0 12px rgba(0,229,255,.6), inset 0 0 12px rgba(0,229,255,.15);
  44. }
  45. .btn:hover{background:var(--neon-cyan);color:#000;
  46. box-shadow:0 0 25px var(--neon-cyan),0 0 60px var(--neon-cyan);transform:translateY(-2px);}
  47. .btn.pink{color:var(--neon-pink);border-color:var(--neon-pink);
  48. box-shadow:0 0 12px rgba(255,46,196,.6), inset 0 0 12px rgba(255,46,196,.15);}
  49. .btn.pink:hover{background:var(--neon-pink);color:#000;box-shadow:0 0 25px var(--neon-pink);}
  50. .btn.green{color:var(--neon-green);border-color:var(--neon-green);}
  51. .btn.green:hover{background:var(--neon-green);color:#000;box-shadow:0 0 25px var(--neon-green);}
  52. .btn:disabled{opacity:.35;cursor:not-allowed;transform:none;}
  53. .topbar{
  54. position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;
  55. align-items:center;padding:10px 16px;z-index:10;font-size:.8rem;
  56. letter-spacing:2px;text-transform:uppercase;color:var(--neon-cyan);
  57. text-shadow:0 0 8px var(--neon-cyan);
  58. }
  59. .topbar .progress{flex:1;height:4px;margin:0 16px;background:rgba(255,255,255,.1);
  60. border-radius:2px;overflow:hidden;}
  61. .topbar .progress div{height:100%;width:0%;
  62. background:linear-gradient(90deg,var(--neon-pink),var(--neon-cyan));
  63. box-shadow:0 0 12px var(--neon-cyan);transition:width .5s;}
  64. .icon-btn{background:transparent;border:1px solid var(--neon-cyan);color:var(--neon-cyan);
  65. border-radius:6px;padding:4px 10px;cursor:pointer;font-size:.75rem;}
  66. .icon-btn:hover{background:var(--neon-cyan);color:#000;}
  67. canvas{display:block;max-width:100%;}
  68. .canvas-wrap{border:1px solid rgba(0,229,255,.3);border-radius:12px;
  69. box-shadow:0 0 30px rgba(162,89,255,.35), inset 0 0 30px rgba(0,229,255,.1);
  70. background:rgba(0,0,0,.35);padding:8px;margin:16px 0;}
  71. .controls{display:flex;flex-wrap:wrap;justify-content:center;gap:12px;margin:16px 0;}
  72. .slider-box{display:flex;flex-direction:column;align-items:center;
  73. background:rgba(255,255,255,.04);padding:10px 16px;border-radius:10px;
  74. border:1px solid rgba(255,46,196,.35);}
  75. .slider-box label{font-size:.75rem;letter-spacing:2px;text-transform:uppercase;
  76. color:var(--neon-pink);margin-bottom:6px;}
  77. input[type=range]{-webkit-appearance:none;width:160px;height:4px;
  78. background:linear-gradient(90deg,var(--neon-pink),var(--neon-cyan));
  79. border-radius:2px;outline:none;}
  80. input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;
  81. border-radius:50%;background:#fff;box-shadow:0 0 12px var(--neon-cyan);cursor:pointer;}
  82. input[type=range]::-moz-range-thumb{width:16px;height:16px;border:none;border-radius:50%;
  83. background:#fff;box-shadow:0 0 12px var(--neon-cyan);cursor:pointer;}
  84. .tubes{display:flex;flex-direction:column;gap:12px;width:min(760px,100%);margin:12px 0;}
  85. .tube{position:relative;height:44px;border-radius:22px;background:rgba(255,255,255,.05);
  86. border:1px solid rgba(0,229,255,.3);overflow:hidden;}
  87. .tube .label{position:absolute;left:12px;top:50%;transform:translateY(-50%);
  88. font-size:.8rem;letter-spacing:2px;color:var(--neon-cyan);z-index:2;}
  89. .tube .impulse{position:absolute;top:50%;left:0;transform:translate(-50%,-50%);
  90. width:20px;height:20px;border-radius:50%;
  91. background:radial-gradient(circle,#fff,var(--neon-pink));
  92. box-shadow:0 0 20px var(--neon-pink);opacity:0;}
  93. .tube .impulse.run{animation:run linear forwards;}
  94. @keyframes run{0%{left:0;opacity:1;}100%{left:100%;opacity:1;}}
  95. .quiz-options{display:flex;flex-wrap:wrap;gap:12px;justify-content:center;margin-top:16px;}
  96. .quiz-options .btn{min-width:120px;}
  97. .quiz-options .btn.correct{background:var(--neon-green);color:#000;
  98. border-color:var(--neon-green);box-shadow:0 0 25px var(--neon-green);}
  99. .quiz-options .btn.wrong{background:#ff2e4c;color:#fff;border-color:#ff2e4c;}
  100. .modal{position:absolute;inset:0;background:rgba(0,0,0,.75);display:none;
  101. align-items:center;justify-content:center;z-index:50;backdrop-filter:blur(4px);}
  102. .modal.active{display:flex;}
  103. .modal-content{background:linear-gradient(145deg,#160028,#0a0014);
  104. border:1px solid var(--neon-cyan);border-radius:14px;padding:28px;
  105. max-width:520px;width:90%;box-shadow:0 0 40px rgba(0,229,255,.5);text-align:center;}
  106. .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));
  107. gap:14px;width:min(800px,100%);margin:16px 0;}
  108. .card{perspective:1000px;height:130px;cursor:pointer;}
  109. .card-inner{position:relative;width:100%;height:100%;transition:transform .6s;
  110. transform-style:preserve-3d;}
  111. .card.flipped .card-inner{transform:rotateY(180deg);}
  112. .card-face{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  113. border-radius:12px;padding:12px;text-align:center;backface-visibility:hidden;
  114. border:1px solid var(--neon-purple);
  115. background:linear-gradient(145deg,rgba(162,89,255,.15),rgba(0,229,255,.08));
  116. box-shadow:0 0 20px rgba(162,89,255,.4);font-weight:700;}
  117. .card-face.back{transform:rotateY(180deg);font-weight:400;font-size:.85rem;
  118. border-color:var(--neon-cyan);color:#cfeaff;}
  119. .pairs{display:grid;grid-template-columns:1fr 1fr;gap:24px;width:min(700px,100%);margin:16px 0;}
  120. .pair-col{display:flex;flex-direction:column;gap:10px;}
  121. .pair-item{padding:10px 14px;border-radius:8px;cursor:pointer;
  122. border:1px solid var(--neon-cyan);background:rgba(0,229,255,.05);
  123. text-align:center;font-size:.9rem;transition:.2s;}
  124. .pair-item:hover{background:rgba(0,229,255,.2);}
  125. .pair-item.selected{background:var(--neon-pink);color:#000;}
  126. .pair-item.matched{opacity:.35;pointer-events:none;border-color:var(--neon-green);}
  127. .molecules{position:relative;width:min(700px,100%);height:120px;
  128. border:1px dashed rgba(0,229,255,.4);border-radius:12px;margin:16px 0;
  129. background:rgba(0,0,0,.3);}
  130. .molecule{position:absolute;top:50%;transform:translate(-50%,-50%);
  131. width:26px;height:26px;border-radius:50%;
  132. background:radial-gradient(circle,#fff,var(--neon-cyan));
  133. box-shadow:0 0 15px var(--neon-cyan);cursor:pointer;transition:left .3s;}
  134. .molecule.placed{background:radial-gradient(circle,#fff,var(--neon-green));
  135. box-shadow:0 0 15px var(--neon-green);}
  136. .target{position:absolute;top:50%;transform:translate(-50%,-50%);
  137. width:30px;height:30px;border-radius:50%;border:2px dashed var(--neon-pink);}
  138. .progress-quiz{width:min(600px,100%);height:6px;border-radius:3px;
  139. background:rgba(255,255,255,.1);margin:16px 0;overflow:hidden;}
  140. .progress-quiz div{height:100%;width:0;
  141. background:linear-gradient(90deg,var(--neon-pink),var(--neon-cyan));
  142. transition:width .4s;}
  143. .question{font-size:1.15rem;text-align:center;max-width:700px;margin:16px 0;line-height:1.5;}
  144. .result-score{font-size:4rem;font-weight:900;color:#fff;
  145. text-shadow:0 0 20px var(--neon-pink),0 0 40px var(--neon-purple);}
  146. .hint{font-size:.8rem;color:var(--neon-pink);letter-spacing:1px;
  147. margin-top:8px;min-height:1.2em;}
  148. .table{font-size:.85rem;color:#cfeaff;margin-top:8px;text-align:center;line-height:1.8;}
  149. .table span{color:var(--neon-cyan);font-weight:700;}
  150. </style>
  151. </head>
  152. <body>
  153. <div id="app">
  154. <div class="topbar">
  155. <span id="sceneLabel">СЦЕНА 1 / 7</span>
  156. <div class="progress"><div id="progressBar"></div></div>
  157. <button class="icon-btn" id="soundToggle" type="button">🔊 ЗВУК</button>
  158. </div>
  159. <!-- SCENE 1 -->
  160. <section class="scene active" id="scene1">
  161. <canvas id="bgCanvas" width="900" height="500"
  162. style="position:absolute;top:0;left:0;width:100%;height:100%;opacity:.35;pointer-events:none;"></canvas>
  163. <div style="position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;">
  164. <h1>Механические волны</h1>
  165. <p>Интерактивный курс: колебания, звук и среды распространения.</p>
  166. <p class="hint">Наушники приветствуются 🎧</p>
  167. <div style="margin-top:24px;">
  168. <button class="btn" id="btnStart" type="button">▶ Начать курс</button>
  169. <button class="btn pink" id="btnWhat" type="button">Что такое волна?</button>
  170. </div>
  171. </div>
  172. </section>
  173. <!-- SCENE 2 -->
  174. <section class="scene" id="scene2">
  175. <h2>Что такое механическая волна?</h2>
  176. <p>Это распространение колебаний в упругой среде. Частицы колеблются, но не переносятся вместе с волной.</p>
  177. <div class="canvas-wrap"><canvas id="chainCanvas" width="800" height="200"></canvas></div>
  178. <div class="controls">
  179. <div class="slider-box">
  180. <label>Частота: <span id="freqVal">3</span> Гц</label>
  181. <input type="range" id="freqSlider" min="1" max="10" value="3">
  182. </div>
  183. <div class="slider-box">
  184. <label>Амплитуда: <span id="ampVal">25</span> px</label>
  185. <input type="range" id="ampSlider" min="5" max="40" value="25">
  186. </div>
  187. <button class="btn green" id="btnPush" type="button">Толкнуть</button>
  188. </div>
  189. <p class="hint" id="scene2Hint">Толчков: 0 / 3</p>
  190. <p class="hint" id="scene2Tone" style="color:var(--neon-cyan);">Тон динамика: 1320 Гц</p>
  191. <button class="btn" id="btnNext2" type="button" disabled>Далее →</button>
  192. </section>
  193. <!-- SCENE 3 -->
  194. <section class="scene" id="scene3">
  195. <h2>Среда распространения. Скорость звука</h2>
  196. <p>Чем плотнее и упруже среда — тем быстрее идёт звук.</p>
  197. <div class="tubes">
  198. <div class="tube"><span class="label">Воздух ≈ 343 м/с</span><div class="impulse" data-speed="2.4"></div></div>
  199. <div class="tube"><span class="label">Вода ≈ 1480 м/с</span><div class="impulse" data-speed="1.2"></div></div>
  200. <div class="tube"><span class="label">Сталь ≈ 5100 м/с</span><div class="impulse" data-speed="0.5"></div></div>
  201. </div>
  202. <button class="btn" id="btnImpulse" type="button">Запустить импульс</button>
  203. <p class="question" style="margin-top:20px;">В какой среде звук распространяется быстрее всего?</p>
  204. <div class="quiz-options" id="quiz3">
  205. <button class="btn" data-ans="air" type="button">Воздух</button>
  206. <button class="btn" data-ans="water" type="button">Вода</button>
  207. <button class="btn" data-ans="steel" type="button">Сталь</button>
  208. </div>
  209. <button class="btn" id="btnNext3" type="button" disabled style="margin-top:20px;">Далее →</button>
  210. </section>
  211. <!-- SCENE 4 -->
  212. <section class="scene" id="scene4">
  213. <h2>Колебания: период, частота, амплитуда</h2>
  214. <div style="display:flex;flex-wrap:wrap;justify-content:center;gap:16px;">
  215. <div class="canvas-wrap"><canvas id="oscCanvas" width="360" height="260"></canvas></div>
  216. <div class="canvas-wrap"><canvas id="waveCanvas" width="360" height="260"></canvas></div>
  217. </div>
  218. <div class="controls">
  219. <div class="slider-box">
  220. <label>Амплитуда: <span id="ampVal2">60</span></label>
  221. <input type="range" id="ampSlider2" min="20" max="100" value="60">
  222. </div>
  223. <div class="slider-box">
  224. <label>Период: <span id="perVal">1.2</span> с</label>
  225. <input type="range" id="perSlider" min="4" max="30" value="12">
  226. </div>
  227. <button class="btn pink" id="btnCheck4" type="button">Проверить понимание</button>
  228. </div>
  229. <p class="table">Формулы: <span>T = 1/ν</span> · <span>ν = 1/T</span> · <span>v = λν</span></p>
  230. <button class="btn" id="btnNext4" type="button" disabled>Далее →</button>
  231. </section>
  232. <!-- SCENE 5 -->
  233. <section class="scene" id="scene5">
  234. <h2>Виды волн. Звук — продольная волна</h2>
  235. <div class="controls">
  236. <button class="btn" id="btnLong" type="button">Продольная</button>
  237. <button class="btn pink" id="btnTrans" type="button">Поперечная</button>
  238. </div>
  239. <div class="canvas-wrap"><canvas id="typeCanvas" width="800" height="200"></canvas></div>
  240. <p>Собери «звук»: кликни по молекуле — она встанет в ближайшую позицию сжатия/разрежения.</p>
  241. <div class="molecules" id="molecules"></div>
  242. <p class="hint" id="scene5Hint">Размещено: 0 / 6</p>
  243. <button class="btn" id="btnNext5" type="button" disabled>Далее →</button>
  244. </section>
  245. <!-- SCENE 6 -->
  246. <section class="scene" id="scene6">
  247. <h2>Применение звуковых волн</h2>
  248. <p>Нажми на карточку, чтобы узнать подробнее.</p>
  249. <div class="cards" id="cards">
  250. <div class="card"><div class="card-inner">
  251. <div class="card-face">Эхолокация</div>
  252. <div class="card-face back">Летучие мыши и дельфины определяют препятствия по отражённому ультразвуку.</div>
  253. </div></div>
  254. <div class="card"><div class="card-inner">
  255. <div class="card-face">УЗИ</div>
  256. <div class="card-face back">Медицинская визуализация: ультразвук отражается от границ тканей.</div>
  257. </div></div>
  258. <div class="card"><div class="card-inner">
  259. <div class="card-face">Сейсмология</div>
  260. <div class="card-face back">По сейсмическим волнам изучают строение Земли и землетрясения.</div>
  261. </div></div>
  262. <div class="card"><div class="card-inner">
  263. <div class="card-face">Музыка</div>
  264. <div class="card-face back">Струны и столбы воздуха создают стоячие волны — ноты.</div>
  265. </div></div>
  266. </div>
  267. <p>Собери пару: пример ↔ физическое явление.</p>
  268. <div class="pairs">
  269. <div class="pair-col" id="pairLeft"></div>
  270. <div class="pair-col" id="pairRight"></div>
  271. </div>
  272. <p class="hint" id="scene6Hint">Собрано пар: 0 / 4</p>
  273. <button class="btn" id="btnNext6" type="button" disabled>К тестированию →</button>
  274. </section>
  275. <!-- SCENE 7 -->
  276. <section class="scene" id="scene7">
  277. <h2 id="quizTitle">Итоговое тестирование</h2>
  278. <div class="progress-quiz"><div id="quizProgress"></div></div>
  279. <p class="question" id="quizQuestion">…</p>
  280. <div class="quiz-options" id="quizOptions"></div>
  281. <div id="quizResult" style="display:none;flex-direction:column;align-items:center;">
  282. <div class="result-score" id="finalScore">0</div>
  283. <p id="finalText" style="margin:16px 0;"></p>
  284. <button class="btn pink" id="btnRestart" type="button">Пройти заново</button>
  285. </div>
  286. </section>
  287. <!-- MODAL -->
  288. <div class="modal" id="modal">
  289. <div class="modal-content">
  290. <h2 id="modalTitle">…</h2>
  291. <p id="modalText" style="margin:16px 0;">…</p>
  292. <button class="btn" id="modalClose" type="button">Закрыть</button>
  293. </div>
  294. </div>
  295. </div>
  296. <script>
  297. /* ================= ЯДРО: НАВИГАЦИЯ + ЗВУК + МОДАЛКА ================= */
  298. var AudioCtx = window.AudioContext || window.webkitAudioContext;
  299. var audioCtx = null;
  300. var soundOn = true;
  301. function ensureAudio(){
  302. try{
  303. if(!audioCtx) audioCtx = new AudioCtx();
  304. if(audioCtx.state === 'suspended') audioCtx.resume();
  305. }catch(e){}
  306. }
  307. function beep(freq, dur, type, vol){
  308. if(!soundOn || !audioCtx) return;
  309. try{
  310. var o = audioCtx.createOscillator();
  311. var g = audioCtx.createGain();
  312. o.type = type || 'sine';
  313. o.frequency.value = freq || 440;
  314. g.gain.setValueAtTime(vol || 0.12, audioCtx.currentTime);
  315. g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + (dur || 0.15));
  316. o.connect(g); g.connect(audioCtx.destination);
  317. o.start(); o.stop(audioCtx.currentTime + (dur || 0.15));
  318. }catch(e){}
  319. }
  320. function correctSound(){ beep(880,0.12,'triangle',0.18); setTimeout(function(){ beep(1320,0.15,'triangle',0.18); },110); }
  321. function wrongSound(){ beep(220,0.25,'sawtooth',0.15); }
  322. function clickSound(){ beep(600,0.06,'square',0.08); }
  323. var scenes = ['scene1','scene2','scene3','scene4','scene5','scene6','scene7'];
  324. var currentScene = 0;
  325. function goToScene(idx){
  326. if(idx < 0 || idx >= scenes.length) return;
  327. var all = document.querySelectorAll('.scene');
  328. for(var i=0;i<all.length;i++) all[i].classList.remove('active');
  329. document.getElementById(scenes[idx]).classList.add('active');
  330. currentScene = idx;
  331. document.getElementById('sceneLabel').textContent = 'СЦЕНА ' + (idx+1) + ' / ' + scenes.length;
  332. document.getElementById('progressBar').style.width = (idx/(scenes.length-1)*100) + '%';
  333. clickSound();
  334. if(idx === 3 && typeof startOscillators === 'function') startOscillators();
  335. if(idx === 4 && typeof initMolecules === 'function') initMolecules();
  336. if(idx === 5 && typeof initPairs === 'function') initPairs();
  337. if(idx === 6 && typeof startQuiz === 'function') startQuiz();
  338. }
  339. var modal = document.getElementById('modal');
  340. function openModal(title, text){
  341. document.getElementById('modalTitle').textContent = title;
  342. document.getElementById('modalText').textContent = text;
  343. modal.classList.add('active');
  344. }
  345. document.getElementById('modalClose').addEventListener('click', function(){
  346. modal.classList.remove('active'); clickSound();
  347. });
  348. modal.addEventListener('click', function(e){ if(e.target === modal) modal.classList.remove('active'); });
  349. document.getElementById('soundToggle').addEventListener('click', function(e){
  350. soundOn = !soundOn;
  351. e.target.textContent = soundOn ? '🔊 ЗВУК' : '🔇 ВЫКЛ';
  352. if(soundOn){ ensureAudio(); clickSound(); }
  353. });
  354. /* ================= ФОН ================= */
  355. var bgCanvas = document.getElementById('bgCanvas');
  356. var bgCtx = bgCanvas.getContext('2d');
  357. var bgT = 0;
  358. function drawBg(){
  359. var W = bgCanvas.width, H = bgCanvas.height;
  360. bgCtx.clearRect(0,0,W,H);
  361. for(var k=0;k<3;k++){
  362. bgCtx.beginPath();
  363. bgCtx.strokeStyle = k===0 ? 'rgba(255,46,196,.5)' : k===1 ? 'rgba(0,229,255,.5)' : 'rgba(162,89,255,.5)';
  364. bgCtx.lineWidth = 2;
  365. bgCtx.shadowBlur = 20;
  366. bgCtx.shadowColor = bgCtx.strokeStyle;
  367. for(var x=0;x<W;x+=4){
  368. var y = H/2 + Math.sin(x*0.02 + bgT + k*1.3) * (60 + k*20);
  369. if(x===0) bgCtx.moveTo(x,y); else bgCtx.lineTo(x,y);
  370. }
  371. bgCtx.stroke();
  372. }
  373. bgCtx.shadowBlur = 0;
  374. bgT += 0.03;
  375. requestAnimationFrame(drawBg);
  376. }
  377. drawBg();
  378. /* ================= ГЛАВНАЯ: КНОПКИ ================= */
  379. document.getElementById('btnStart').addEventListener('click', function(){
  380. ensureAudio();
  381. clickSound();
  382. goToScene(1); // ← ПЕРЕХОД НА СЦЕНУ 2 (начало курса)
  383. });
  384. document.getElementById('btnWhat').addEventListener('click', function(){
  385. ensureAudio();
  386. clickSound();
  387. openModal(
  388. 'Что такое волна?',
  389. 'Механическая волна — это процесс распространения колебаний в упругой среде. Частицы среды колеблются около положения равновесия, а сама волна переносит энергию, но не вещество. Примеры: звук в воздухе, волны на воде, сейсмические волны в Земле.'
  390. );
  391. });
  392. /* ================= SCENE 2: цепочка ================= */
  393. (function(){
  394. var c = document.getElementById('chainCanvas');
  395. var ctx = c.getContext('2d');
  396. var t = 0, amp = 25, freq = 3, pushTime = -999, pushes = 0;
  397. // === Частота звука динамика: 1 Гц бегунка → 440 Гц, 10 Гц → 4400 Гц ===
  398. function toneHz(){ return freq * 440; }
  399. function updateToneLabel(){
  400. var lbl = document.getElementById('scene2Tone');
  401. if(lbl) lbl.textContent = 'Тон динамика: ' + toneHz() + ' Гц';
  402. }
  403. updateToneLabel();
  404. document.getElementById('freqSlider').addEventListener('input', function(e){
  405. freq = +e.target.value;
  406. document.getElementById('freqVal').textContent = freq;
  407. updateToneLabel();
  408. // короткий «щелчок» динамика на новой частоте
  409. ensureAudio();
  410. beep(toneHz(), 0.18, 'sine', 0.18);
  411. });
  412. document.getElementById('ampSlider').addEventListener('input', function(e){
  413. amp = +e.target.value;
  414. document.getElementById('ampVal').textContent = amp;
  415. });
  416. document.getElementById('btnPush').addEventListener('click', function(){
  417. pushTime = t; pushes++;
  418. document.getElementById('scene2Hint').textContent = 'Толчков: ' + pushes + ' / 3';
  419. // Тон соответствует положению бегунка частоты
  420. ensureAudio();
  421. beep(toneHz(), 0.25, 'sine', 0.22);
  422. if(pushes >= 3){
  423. document.getElementById('btnNext2').disabled = false;
  424. document.getElementById('scene2Hint').textContent = 'Отлично! Можно идти дальше.';
  425. }
  426. });
  427. document.getElementById('btnNext2').addEventListener('click', function(){ goToScene(2); });
  428. function loop(){
  429. var W = c.width, H = c.height;
  430. ctx.clearRect(0,0,W,H);
  431. var N = 12, spacing = W/(N+1);
  432. t += 0.03;
  433. var pts = [];
  434. for(var i=0;i<N;i++){
  435. var x = spacing*(i+1);
  436. var delay = i*0.25/Math.max(freq,1);
  437. var dt = t - pushTime - delay;
  438. var off;
  439. if(dt > 0){
  440. off = Math.sin(dt*freq*2*Math.PI) * amp * Math.exp(-dt*1.2);
  441. } else {
  442. off = Math.sin(t*freq*0.5) * amp * 0.15;
  443. }
  444. pts.push({x:x, y:H/2 + off});
  445. }
  446. ctx.strokeStyle = 'rgba(0,229,255,.5)';
  447. ctx.lineWidth = 2;
  448. ctx.shadowBlur = 10; ctx.shadowColor = '#00e5ff';
  449. for(var j=0;j<pts.length-1;j++){
  450. ctx.beginPath();
  451. ctx.moveTo(pts[j].x, pts[j].y);
  452. ctx.lineTo(pts[j+1].x, pts[j+1].y);
  453. ctx.stroke();
  454. }
  455. for(var k=0;k<pts.length;k++){
  456. ctx.beginPath();
  457. ctx.fillStyle = '#ff2ec4';
  458. ctx.shadowBlur = 20; ctx.shadowColor = '#ff2ec4';
  459. ctx.arc(pts[k].x, pts[k].y, 10, 0, Math.PI*2);
  460. ctx.fill();
  461. }
  462. ctx.shadowBlur = 0;
  463. requestAnimationFrame(loop);
  464. }
  465. loop();
  466. })();
  467. /* ================= SCENE 3: трубы + квиз ================= */
  468. document.getElementById('btnImpulse').addEventListener('click', function(){
  469. clickSound();
  470. var imps = document.querySelectorAll('#scene3 .impulse');
  471. for(var i=0;i<imps.length;i++){
  472. imps[i].style.animationDuration = imps[i].dataset.speed + 's';
  473. imps[i].classList.remove('run');
  474. void imps[i].offsetWidth;
  475. imps[i].classList.add('run');
  476. }
  477. });
  478. var q3 = document.querySelectorAll('#quiz3 .btn');
  479. for(var qi=0; qi<q3.length; qi++){
  480. (function(b){
  481. b.addEventListener('click', function(){
  482. if(b.dataset.ans === 'steel'){
  483. b.classList.add('correct'); correctSound();
  484. document.getElementById('btnNext3').disabled = false;
  485. } else {
  486. b.classList.add('wrong'); wrongSound();
  487. }
  488. });
  489. })(q3[qi]);
  490. }
  491. document.getElementById('btnNext3').addEventListener('click', function(){ goToScene(3); });
  492. /* ================= SCENE 4: маятник + волна ================= */
  493. var oscCanvas = document.getElementById('oscCanvas');
  494. var oscCtx = oscCanvas.getContext('2d');
  495. var waveCanvas = document.getElementById('waveCanvas');
  496. var waveCtx = waveCanvas.getContext('2d');
  497. var oscT = 0, oscAmp = 60, oscPeriod = 1.2;
  498. var oscRunning = false;
  499. document.getElementById('ampSlider2').addEventListener('input', function(e){
  500. oscAmp = +e.target.value;
  501. document.getElementById('ampVal2').textContent = oscAmp;
  502. });
  503. document.getElementById('perSlider').addEventListener('input', function(e){
  504. oscPeriod = +e.target.value / 10;
  505. document.getElementById('perVal').textContent = oscPeriod.toFixed(1);
  506. });
  507. function startOscillators(){
  508. if(oscRunning) return;
  509. oscRunning = true;
  510. oscT = 0;
  511. requestAnimationFrame(drawOsc);
  512. }
  513. function drawOsc(){
  514. oscT += 0.04;
  515. var W = oscCanvas.width, H = oscCanvas.height;
  516. oscCtx.clearRect(0,0,W,H);
  517. // ось
  518. oscCtx.strokeStyle = 'rgba(0,229,255,.3)';
  519. oscCtx.lineWidth = 1;
  520. oscCtx.beginPath(); oscCtx.moveTo(W/2,30); oscCtx.lineTo(W/2,H-30); oscCtx.stroke();
  521. // маятник
  522. var ang = Math.sin(oscT* (2*Math.PI/oscPeriod)) * (oscAmp/200);
  523. var len = 120, cx = W/2, cy = 40;
  524. var bx = cx + Math.sin(ang)*len;
  525. var by = cy + Math.cos(ang)*len;
  526. oscCtx.strokeStyle = '#a259ff';
  527. oscCtx.lineWidth = 2;
  528. oscCtx.shadowBlur = 12; oscCtx.shadowColor = '#a259ff';
  529. oscCtx.beginPath(); oscCtx.moveTo(cx, cy); oscCtx.lineTo(bx, by); oscCtx.stroke();
  530. oscCtx.beginPath();
  531. oscCtx.fillStyle = '#ff2ec4';
  532. oscCtx.shadowBlur = 25; oscCtx.shadowColor = '#ff2ec4';
  533. oscCtx.arc(bx, by, 14, 0, Math.PI*2);
  534. oscCtx.fill();
  535. oscCtx.shadowBlur = 0;
  536. // правый — осциллограмма волны
  537. var WW = waveCanvas.width, WH = waveCanvas.height;
  538. waveCtx.clearRect(0,0,WW,WH);
  539. waveCtx.strokeStyle = 'rgba(0,229,255,.2)';
  540. waveCtx.lineWidth = 1;
  541. waveCtx.beginPath(); waveCtx.moveTo(0, WH/2); waveCtx.lineTo(WW, WH/2); waveCtx.stroke();
  542. waveCtx.strokeStyle = '#00e5ff';
  543. waveCtx.lineWidth = 2;
  544. waveCtx.shadowBlur = 15; waveCtx.shadowColor = '#00e5ff';
  545. waveCtx.beginPath();
  546. for(var x=0; x<WW; x++){
  547. var y = WH/2 + Math.sin((x/WW)*Math.PI*4 + oscT*(2*Math.PI/oscPeriod)) * oscAmp;
  548. if(x===0) waveCtx.moveTo(x,y); else waveCtx.lineTo(x,y);
  549. }
  550. waveCtx.stroke();
  551. waveCtx.shadowBlur = 0;
  552. requestAnimationFrame(drawOsc);
  553. }
  554. document.getElementById('btnCheck4').addEventListener('click', function(){
  555. ensureAudio();
  556. openModal(
  557. 'Проверь себя',
  558. 'Если период T = 2 с, то частота ν = 1/T = 0,5 Гц. А если частота ν = 4 Гц, то период T = 1/ν = 0,25 с. Запомни: T и ν — взаимно обратные величины.'
  559. );
  560. document.getElementById('btnNext4').disabled = false;
  561. correctSound();
  562. });
  563. document.getElementById('btnNext4').addEventListener('click', function(){ goToScene(4); });
  564. /* ================= SCENE 5: виды волн + молекулы ================= */
  565. var typeCanvas = document.getElementById('typeCanvas');
  566. var typeCtx = typeCanvas.getContext('2d');
  567. var typeT = 0;
  568. var waveType = 'long'; // long | trans
  569. document.getElementById('btnLong').addEventListener('click', function(){
  570. waveType = 'long'; clickSound(); initMolecules(); resetNext5();
  571. });
  572. document.getElementById('btnTrans').addEventListener('click', function(){
  573. waveType = 'trans'; clickSound(); initMolecules(); resetNext5();
  574. });
  575. function resetNext5(){
  576. document.getElementById('btnNext5').disabled = true;
  577. document.getElementById('scene5Hint').textContent = 'Размещено: 0 / 6';
  578. }
  579. function drawType(){
  580. var W = typeCanvas.width, H = typeCanvas.height;
  581. typeCtx.clearRect(0,0,W,H);
  582. typeT += 0.05;
  583. if(waveType === 'long'){
  584. // продольная — сжатия/разрежения
  585. var N = 40;
  586. for(var i=0;i<N;i++){
  587. var baseX = (i+1) * (W/(N+1));
  588. var shift = Math.sin(typeT - i*0.3) * 12;
  589. var x = baseX + shift;
  590. var density = 0.5 + 0.5 * Math.sin(typeT - i*0.3);
  591. typeCtx.beginPath();
  592. typeCtx.fillStyle = 'rgba(255,46,196,' + (0.4 + density*0.6) + ')';
  593. typeCtx.shadowBlur = 15; typeCtx.shadowColor = '#ff2ec4';
  594. typeCtx.arc(x, H/2, 6 + density*3, 0, Math.PI*2);
  595. typeCtx.fill();
  596. }
  597. typeCtx.shadowBlur = 0;
  598. typeCtx.fillStyle = '#00e5ff';
  599. typeCtx.font = '14px Segoe UI';
  600. typeCtx.fillText('Продольная (звук в воздухе)', 20, 30);
  601. } else {
  602. // поперечная — синусоида
  603. typeCtx.strokeStyle = 'rgba(0,229,255,.4)';
  604. typeCtx.lineWidth = 2;
  605. typeCtx.beginPath();
  606. for(var x2=0; x2<W; x2+=4){
  607. var y2 = H/2 + Math.sin(x2*0.02 + typeT) * 50;
  608. if(x2===0) typeCtx.moveTo(x2,y2); else typeCtx.lineTo(x2,y2);
  609. }
  610. typeCtx.stroke();
  611. for(var k=0;k<20;k++){
  612. var xx = (k+1) * (W/21);
  613. var yy = H/2 + Math.sin(xx*0.02 + typeT) * 50;
  614. typeCtx.beginPath();
  615. typeCtx.fillStyle = '#ff2ec4';
  616. typeCtx.shadowBlur = 15; typeCtx.shadowColor = '#ff2ec4';
  617. typeCtx.arc(xx, yy, 6, 0, Math.PI*2);
  618. typeCtx.fill();
  619. }
  620. typeCtx.shadowBlur = 0;
  621. typeCtx.fillStyle = '#00e5ff';
  622. typeCtx.font = '14px Segoe UI';
  623. typeCtx.fillText('Поперечная (волны на воде, струна)', 20, 30);
  624. }
  625. requestAnimationFrame(drawType);
  626. }
  627. drawType();
  628. /* молекулы */
  629. var molPlaced = 0;
  630. function initMolecules(){
  631. var box = document.getElementById('molecules');
  632. box.innerHTML = '';
  633. molPlaced = 0;
  634. var W = box.clientWidth || 700;
  635. // цели (позиции сжатия/разрежения)
  636. var targetPos = [0.12, 0.25, 0.40, 0.55, 0.70, 0.85];
  637. window._molTargets = targetPos.map(function(p){ return p*W; });
  638. // пунктирные цели
  639. targetPos.forEach(function(p){
  640. var t = document.createElement('div');
  641. t.className = 'target';
  642. t.style.left = (p*100) + '%';
  643. box.appendChild(t);
  644. });
  645. // молекулы в случайных позициях
  646. for(var i=0;i<6;i++){
  647. (function(idx){
  648. var m = document.createElement('div');
  649. m.className = 'molecule';
  650. var startPct = [0.05, 0.20, 0.35, 0.50, 0.65, 0.80][idx] * 100;
  651. // перемешаем стартовые
  652. startPct = 5 + Math.random()*90;
  653. m.style.left = startPct + '%';
  654. m.dataset.idx = idx;
  655. m.addEventListener('click', function(){
  656. if(m.classList.contains('placed')) return;
  657. // ставим в ближайшую незанятую цель
  658. var used = {};
  659. var placed = document.querySelectorAll('#molecules .molecule.placed');
  660. for(var j=0;j<placed.length;j++) used[placed[j].dataset.target] = true;
  661. var best = -1, bestDist = 1e9;
  662. for(var k=0;k<targetPos.length;k++){
  663. if(used[k]) continue;
  664. var d = Math.abs(targetPos[k]*100 - parseFloat(m.style.left));
  665. if(d < bestDist){ bestDist = d; best = k; }
  666. }
  667. if(best === -1) return;
  668. m.style.left = (targetPos[best]*100) + '%';
  669. m.dataset.target = best;
  670. m.classList.add('placed');
  671. molPlaced++;
  672. beep(500 + molPlaced*80, 0.1, 'triangle', 0.15);
  673. document.getElementById('scene5Hint').textContent = 'Размещено: ' + molPlaced + ' / 6';
  674. if(molPlaced >= 6){
  675. document.getElementById('btnNext5').disabled = false;
  676. document.getElementById('scene5Hint').textContent = 'Отлично! Звук собран 🎵';
  677. correctSound();
  678. }
  679. });
  680. box.appendChild(m);
  681. })(i);
  682. }
  683. }
  684. document.getElementById('btnNext5').addEventListener('click', function(){ goToScene(5); });
  685. /* ================= SCENE 6: карточки + пары ================= */
  686. var pairData = [
  687. { left: 'Эхолокация', right: 'Отражение ультразвука' },
  688. { left: 'УЗИ', right: 'Отражение от тканей' },
  689. { left: 'Сейсмология', right: 'Сейсмические волны' },
  690. { left: 'Музыка', right: 'Стоячие волны' }
  691. ];
  692. var pairsDone = 0;
  693. var selectedLeft = null;
  694. function initPairs(){
  695. pairsDone = 0;
  696. selectedLeft = null;
  697. document.getElementById('scene6Hint').textContent = 'Собрано пар: 0 / 4';
  698. document.getElementById('btnNext6').disabled = true;
  699. var leftCol = document.getElementById('pairLeft');
  700. var rightCol = document.getElementById('pairRight');
  701. leftCol.innerHTML = ''; rightCol.innerHTML = '';
  702. // левая колонка по порядку
  703. pairData.forEach(function(p, i){
  704. var d = document.createElement('div');
  705. d.className = 'pair-item';
  706. d.textContent = p.left;
  707. d.dataset.idx = i;
  708. d.dataset.side = 'left';
  709. d.addEventListener('click', function(){
  710. if(d.classList.contains('matched')) return;
  711. var sel = document.querySelectorAll('#pairLeft .pair-item.selected');
  712. for(var s=0;s<sel.length;s++) sel[s].classList.remove('selected');
  713. d.classList.add('selected');
  714. selectedLeft = d;
  715. clickSound();
  716. });
  717. leftCol.appendChild(d);
  718. });
  719. // правая колонка перемешана
  720. var shuffled = pairData.map(function(p,i){ return {text:p.right, idx:i}; });
  721. shuffled.sort(function(){ return Math.random()-0.5; });
  722. shuffled.forEach(function(item){
  723. var d = document.createElement('div');
  724. d.className = 'pair-item';
  725. d.textContent = item.text;
  726. d.dataset.idx = item.idx;
  727. d.dataset.side = 'right';
  728. d.addEventListener('click', function(){
  729. if(d.classList.contains('matched')) return;
  730. if(!selectedLeft){ wrongSound(); return; }
  731. if(selectedLeft.dataset.idx === d.dataset.idx){
  732. selectedLeft.classList.add('matched');
  733. selectedLeft.classList.remove('selected');
  734. d.classList.add('matched');
  735. selectedLeft = null;
  736. pairsDone++;
  737. correctSound();
  738. document.getElementById('scene6Hint').textContent = 'Собрано пар: ' + pairsDone + ' / 4';
  739. if(pairsDone >= 4){
  740. document.getElementById('btnNext6').disabled = false;
  741. document.getElementById('scene6Hint').textContent = 'Все пары собраны! 🎉';
  742. }
  743. } else {
  744. wrongSound();
  745. d.classList.add('wrong');
  746. setTimeout(function(){ d.classList.remove('wrong'); }, 400);
  747. }
  748. });
  749. rightCol.appendChild(d);
  750. });
  751. }
  752. // карточки — переворот
  753. var cards = document.querySelectorAll('#cards .card');
  754. for(var ci=0; ci<cards.length; ci++){
  755. cards[ci].addEventListener('click', function(){
  756. this.classList.toggle('flipped');
  757. clickSound();
  758. });
  759. }
  760. document.getElementById('btnNext6').addEventListener('click', function(){ goToScene(6); });
  761. /* ================= SCENE 7: ТЕСТ ================= */
  762. var quizQuestions = [
  763. { q: 'Что переносит механическая волна?', a: ['Вещество', 'Энергию', 'Заряд', 'Массу'], c: 1 },
  764. { q: 'В какой среде звук распространяется быстрее всего?', a: ['Воздух', 'Вода', 'Сталь', 'Вакуум'], c: 2 },
  765. { q: 'Формула связи периода и частоты:', a: ['T = ν', 'T = 1/ν', 'T = ν²', 'T = 2ν'], c: 1 },
  766. { q: 'Звук — это какая волна?', a: ['Поперечная', 'Продольная', 'Электромагнитная', 'Стоячая'], c: 1 },
  767. { q: 'Скорость звука в воздухе примерно равна:', a: ['34 м/с', '343 м/с', '3430 м/с', '3 м/с'], c: 1 },
  768. { q: 'Что такое амплитуда?', a: ['Число колебаний в секунду', 'Максимальное отклонение от равновесия', 'Время одного колебания', 'Длина волны'], c: 1 },
  769. { q: 'Единица измерения частоты:', a: ['Гц', 'м', 'с', 'кг'], c: 0 },
  770. { q: 'Могут ли механические волны распространяться в вакууме?', a: ['Да', 'Нет', 'Только звук', 'Только свет'], c: 1 },
  771. { q: 'Связь скорости, длины волны и частоты:', a: ['v = λ/ν', 'v = λ·ν', 'v = ν/λ', 'v = λ+ν'], c: 1 },
  772. { q: 'Ультразвук — это:', a: ['ν < 20 Гц', 'ν > 20 кГц', 'ν = 20 Гц', 'Любой звук'], c: 1 }
  773. ];
  774. var qIndex = 0, qScore = 0;
  775. function startQuiz(){
  776. qIndex = 0; qScore = 0;
  777. document.getElementById('quizResult').style.display = 'none';
  778. document.getElementById('quizQuestion').style.display = 'block';
  779. document.getElementById('quizOptions').style.display = 'flex';
  780. document.getElementById('quizTitle').textContent = 'Итоговое тестирование';
  781. showQuestion();
  782. }
  783. function showQuestion(){
  784. var q = quizQuestions[qIndex];
  785. document.getElementById('quizProgress').style.width = (qIndex / quizQuestions.length * 100) + '%';
  786. document.getElementById('quizQuestion').textContent = (qIndex+1) + '. ' + q.q;
  787. var opts = document.getElementById('quizOptions');
  788. opts.innerHTML = '';
  789. q.a.forEach(function(text, i){
  790. var b = document.createElement('button');
  791. b.className = 'btn';
  792. b.type = 'button';
  793. b.textContent = text;
  794. b.addEventListener('click', function(){
  795. // блокируем остальные
  796. var all = opts.querySelectorAll('.btn');
  797. for(var k=0;k<all.length;k++) all[k].disabled = true;
  798. if(i === q.c){
  799. b.classList.add('correct');
  800. qScore++;
  801. correctSound();
  802. } else {
  803. b.classList.add('wrong');
  804. all[q.c].classList.add('correct');
  805. wrongSound();
  806. }
  807. setTimeout(function(){
  808. qIndex++;
  809. if(qIndex < quizQuestions.length){
  810. showQuestion();
  811. } else {
  812. showResult();
  813. }
  814. }, 900);
  815. });
  816. opts.appendChild(b);
  817. });
  818. }
  819. function showResult(){
  820. document.getElementById('quizProgress').style.width = '100%';
  821. document.getElementById('quizQuestion').style.display = 'none';
  822. document.getElementById('quizOptions').style.display = 'none';
  823. var res = document.getElementById('quizResult');
  824. res.style.display = 'flex';
  825. var pct = Math.round(qScore / quizQuestions.length * 100);
  826. document.getElementById('finalScore').textContent = qScore + ' / ' + quizQuestions.length;
  827. var grade = pct >= 90 ? 'Отлично! 🏆' : pct >= 70 ? 'Хорошо! 👍' : pct >= 50 ? 'Неплохо, но повтори материал.' : 'Попробуй пройти курс заново.';
  828. document.getElementById('finalText').textContent = pct + '% — ' + grade;
  829. if(pct >= 70) correctSound(); else wrongSound();
  830. }
  831. document.getElementById('btnRestart').addEventListener('click', function(){
  832. clickSound();
  833. goToScene(0);
  834. });
  835. </script>
  836. </body>
  837. </html>