project0_67.html 66 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662
  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. /* 🌬️ Воздух — голубой полупрозрачный с облачками */
  95. .tube.air{
  96. background:
  97. radial-gradient(circle at 20% 40%, rgba(255,255,255,.25) 0%, transparent 40%),
  98. radial-gradient(circle at 70% 60%, rgba(255,255,255,.18) 0%, transparent 35%),
  99. linear-gradient(90deg, rgba(180,220,255,.15), rgba(140,200,255,.25), rgba(180,220,255,.15));
  100. border-color: rgba(180,220,255,.6);
  101. box-shadow: inset 0 0 25px rgba(180,220,255,.25);
  102. }
  103. .tube.air .label{color:#bfe3ff; text-shadow:0 0 8px #bfe3ff;}
  104. /* 💧 Вода — сине-бирюзовая с волнами */
  105. .tube.water{
  106. background:
  107. radial-gradient(ellipse at 30% 20%, rgba(0,229,255,.35) 0%, transparent 55%),
  108. radial-gradient(ellipse at 75% 80%, rgba(0,140,255,.35) 0%, transparent 50%),
  109. linear-gradient(90deg, rgba(0,90,180,.5), rgba(0,180,220,.55), rgba(0,90,180,.5));
  110. border-color: rgba(0,229,255,.7);
  111. box-shadow: inset 0 0 30px rgba(0,180,255,.4);
  112. position:relative; overflow:hidden;
  113. }
  114. .tube.water::after{
  115. content:'';
  116. position:absolute;left:-50%;top:50%;width:200%;height:200%;
  117. background:
  118. radial-gradient(ellipse at 20% 50%, rgba(255,255,255,.15) 0%, transparent 40%),
  119. radial-gradient(ellipse at 60% 50%, rgba(255,255,255,.12) 0%, transparent 35%),
  120. radial-gradient(ellipse at 90% 50%, rgba(255,255,255,.15) 0%, transparent 40%);
  121. animation:waterShift 6s linear infinite;
  122. pointer-events:none;
  123. }
  124. @keyframes waterShift{
  125. 0%{transform:translateX(0);}
  126. 100%{transform:translateX(25%);}
  127. }
  128. .tube.water .label{color:#cffaff; text-shadow:0 0 10px #00e5ff;}
  129. /* ⚙️ Сталь — серебристо-металлическая с бликом */
  130. .tube.steel{
  131. background:
  132. linear-gradient(90deg,
  133. rgba(120,130,150,.55) 0%,
  134. rgba(200,210,230,.75) 25%,
  135. rgba(160,170,190,.65) 50%,
  136. rgba(220,230,245,.8) 75%,
  137. rgba(120,130,150,.55) 100%);
  138. border-color: rgba(200,215,235,.85);
  139. box-shadow:
  140. inset 0 0 25px rgba(255,255,255,.25),
  141. inset 0 2px 4px rgba(255,255,255,.4),
  142. inset 0 -2px 4px rgba(0,0,0,.4),
  143. 0 0 15px rgba(200,220,255,.35);
  144. position:relative; overflow:hidden;
  145. }
  146. .tube.steel::after{
  147. content:'';
  148. position:absolute;left:0;top:0;bottom:0;width:30%;
  149. background:linear-gradient(90deg, transparent, rgba(255,255,255,.55), transparent);
  150. animation:steelShine 3.5s ease-in-out infinite;
  151. pointer-events:none;
  152. }
  153. @keyframes steelShine{
  154. 0%{left:-30%;}
  155. 50%{left:100%;}
  156. 100%{left:100%;}
  157. }
  158. .tube.steel .label{
  159. color:#fff; text-shadow:0 0 8px #ffffff, 0 0 4px #000;
  160. font-weight:800;
  161. }
  162. @keyframes run{0%{left:0;opacity:1;}100%{left:100%;opacity:1;}}
  163. .quiz-options{display:flex;flex-wrap:wrap;gap:12px;justify-content:center;margin-top:16px;}
  164. .quiz-options .btn{min-width:120px;}
  165. .quiz-options .btn.correct{background:var(--neon-green);color:#000;
  166. border-color:var(--neon-green);box-shadow:0 0 25px var(--neon-green);}
  167. .quiz-options .btn.wrong{background:#ff2e4c;color:#fff;border-color:#ff2e4c;}
  168. .modal{position:absolute;inset:0;background:rgba(0,0,0,.75);display:none;
  169. align-items:center;justify-content:center;z-index:50;backdrop-filter:blur(4px);}
  170. .modal.active{display:flex;}
  171. .modal-content{background:linear-gradient(145deg,#160028,#0a0014);
  172. border:1px solid var(--neon-cyan);border-radius:14px;padding:28px;
  173. max-width:520px;width:90%;box-shadow:0 0 40px rgba(0,229,255,.5);text-align:center;}
  174. .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));
  175. gap:14px;width:min(800px,100%);margin:16px 0;}
  176. .card{perspective:1000px;height:130px;cursor:pointer;}
  177. .card-inner{position:relative;width:100%;height:100%;transition:transform .6s;
  178. transform-style:preserve-3d;}
  179. .card.flipped .card-inner{transform:rotateY(180deg);}
  180. .card-face{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  181. border-radius:12px;padding:12px;text-align:center;backface-visibility:hidden;
  182. border:1px solid var(--neon-purple);
  183. background:linear-gradient(145deg,rgba(162,89,255,.15),rgba(0,229,255,.08));
  184. box-shadow:0 0 20px rgba(162,89,255,.4);font-weight:700;}
  185. /* 🎨 Карточки с PNG-картинками */
  186. .card-face{
  187. flex-direction: column;
  188. gap: 12px;
  189. padding: 16px;
  190. }
  191. .card-face .card-img{
  192. width: 72px;
  193. height: 72px;
  194. object-fit: contain;
  195. animation: cardFloat 3s ease-in-out infinite;
  196. transition: transform .3s;
  197. }
  198. @keyframes cardFloat{
  199. 0%,100%{transform: translateY(0) scale(1);}
  200. 50%{transform: translateY(-4px) scale(1.04);}
  201. }
  202. .card-face .card-title{
  203. font-size: .95rem;
  204. letter-spacing: 1.5px;
  205. text-transform: uppercase;
  206. font-weight: 800;
  207. }
  208. /* 🦇 Эхолокация — зелёный неон */
  209. .card.eco .card-face{
  210. background: linear-gradient(145deg, rgba(57,255,20,.15), rgba(0,229,255,.06));
  211. border-color: var(--neon-green);
  212. box-shadow: 0 0 25px rgba(57,255,20,.35);
  213. color: var(--neon-green);
  214. }
  215. .card.eco .card-img{
  216. filter: drop-shadow(0 0 14px #39ff14) drop-shadow(0 0 6px #39ff14);
  217. }
  218. .card.eco .card-face.back{
  219. color: #d8ffd0;
  220. border-color: var(--neon-cyan);
  221. }
  222. /* 🩻 УЗИ — циан */
  223. .card.uzi .card-face{
  224. background: linear-gradient(145deg, rgba(0,229,255,.15), rgba(162,89,255,.06));
  225. border-color: var(--neon-cyan);
  226. box-shadow: 0 0 25px rgba(0,229,255,.35);
  227. color: var(--neon-cyan);
  228. }
  229. .card.uzi .card-img{
  230. filter: drop-shadow(0 0 14px #00e5ff) drop-shadow(0 0 6px #00e5ff);
  231. }
  232. .card.uzi .card-face.back{
  233. color: #cfeaff;
  234. border-color: var(--neon-purple);
  235. }
  236. /* 🌍 Сейсмология — фиолетовый */
  237. .card.seismo .card-face{
  238. background: linear-gradient(145deg, rgba(162,89,255,.15), rgba(255,46,196,.06));
  239. border-color: var(--neon-purple);
  240. box-shadow: 0 0 25px rgba(162,89,255,.35);
  241. color: var(--neon-purple);
  242. }
  243. .card.seismo .card-img{
  244. filter: drop-shadow(0 0 14px #a259ff) drop-shadow(0 0 6px #a259ff);
  245. }
  246. .card.seismo .card-face.back{
  247. color: #e8d5ff;
  248. border-color: var(--neon-pink);
  249. }
  250. /* 🎸 Музыка — розовый */
  251. .card.music .card-face{
  252. background: linear-gradient(145deg, rgba(255,46,196,.15), rgba(0,229,255,.06));
  253. border-color: var(--neon-pink);
  254. box-shadow: 0 0 25px rgba(255,46,196,.35);
  255. color: var(--neon-pink);
  256. }
  257. .card.music .card-img{
  258. filter: drop-shadow(0 0 14px #ff2ec4) drop-shadow(0 0 6px #ff2ec4);
  259. }
  260. .card.music .card-face.back{
  261. color: #ffd5f4;
  262. border-color: var(--neon-cyan);
  263. }
  264. /* Задняя сторона — без покачивания */
  265. .card-face.back .card-img{
  266. width: 40px;
  267. height: 40px;
  268. animation: none;
  269. }
  270. .card-face.back{transform:rotateY(180deg);font-weight:400;font-size:.85rem;
  271. border-color:var(--neon-cyan);color:#cfeaff;}
  272. .pairs{display:grid;grid-template-columns:1fr 1fr;gap:24px;width:min(700px,100%);margin:16px 0;}
  273. .pair-col{display:flex;flex-direction:column;gap:10px;}
  274. .pair-item{padding:10px 14px;border-radius:8px;cursor:pointer;
  275. border:1px solid var(--neon-cyan);background:rgba(0,229,255,.05);
  276. text-align:center;font-size:.9rem;transition:.2s;}
  277. .pair-item:hover{background:rgba(0,229,255,.2);}
  278. .pair-item.selected{background:var(--neon-pink);color:#000;}
  279. .pair-item.matched{opacity:.35;pointer-events:none;border-color:var(--neon-green);}
  280. .molecules{position:relative;width:min(700px,100%);height:120px;
  281. border:1px dashed rgba(0,229,255,.4);border-radius:12px;margin:16px 0;
  282. background:rgba(0,0,0,.3);}
  283. .molecule{position:absolute;top:50%;transform:translate(-50%,-50%);
  284. width:26px;height:26px;border-radius:50%;
  285. background:radial-gradient(circle,#fff,var(--neon-cyan));
  286. box-shadow:0 0 15px var(--neon-cyan);cursor:pointer;transition:left .3s;}
  287. .molecule.placed{background:radial-gradient(circle,#fff,var(--neon-green));
  288. box-shadow:0 0 15px var(--neon-green);}
  289. .target{position:absolute;top:50%;transform:translate(-50%,-50%);
  290. width:30px;height:30px;border-radius:50%;border:2px dashed var(--neon-pink);}
  291. .progress-quiz{width:min(600px,100%);height:6px;border-radius:3px;
  292. background:rgba(255,255,255,.1);margin:16px 0;overflow:hidden;}
  293. .progress-quiz div{height:100%;width:0;
  294. background:linear-gradient(90deg,var(--neon-pink),var(--neon-cyan));
  295. transition:width .4s;}
  296. .question{font-size:1.15rem;text-align:center;max-width:700px;margin:16px 0;line-height:1.5;}
  297. .result-score{font-size:4rem;font-weight:900;color:#fff;
  298. text-shadow:0 0 20px var(--neon-pink),0 0 40px var(--neon-purple);}
  299. /* 🎉 финальный экран */
  300. #fxCanvas{
  301. position:absolute;inset:0;width:100%;height:100%;
  302. pointer-events:none;z-index:5;
  303. }
  304. .final-badge{
  305. font-size:5rem;font-weight:900;
  306. letter-spacing:4px;text-transform:uppercase;
  307. text-align:center;margin-bottom:8px;
  308. animation:pop .6s cubic-bezier(.2,1.4,.5,1) both;
  309. }
  310. .final-badge.win{
  311. color:#fff;
  312. text-shadow:
  313. 0 0 10px var(--neon-green),
  314. 0 0 30px var(--neon-green),
  315. 0 0 60px var(--neon-cyan),
  316. 0 0 100px var(--neon-pink);
  317. animation:pop .6s cubic-bezier(.2,1.4,.5,1) both,
  318. winPulse 1.4s ease-in-out infinite .6s;
  319. }
  320. .final-badge.good{
  321. color:#fff;
  322. text-shadow:
  323. 0 0 10px var(--neon-cyan),
  324. 0 0 30px var(--neon-cyan),
  325. 0 0 60px var(--neon-purple);
  326. animation:pop .6s cubic-bezier(.2,1.4,.5,1) both;
  327. }
  328. .final-badge.meh{
  329. color:#fff;
  330. text-shadow:0 0 12px var(--neon-purple),0 0 30px var(--neon-purple);
  331. animation:pop .6s cubic-bezier(.2,1.4,.5,1) both;
  332. }
  333. .final-badge.lose{
  334. color:#fff;
  335. text-shadow:0 0 10px #ff2e4c,0 0 30px #ff2e4c,0 0 60px #ff2e4c;
  336. animation:pop .6s cubic-bezier(.2,1.4,.5,1) both,
  337. losePulse 1.2s ease-in-out infinite .6s;
  338. }
  339. @keyframes pop{
  340. 0%{transform:scale(.3);opacity:0;}
  341. 60%{transform:scale(1.15);opacity:1;}
  342. 100%{transform:scale(1);opacity:1;}
  343. }
  344. @keyframes winPulse{
  345. 0%,100%{transform:scale(1);}
  346. 50%{transform:scale(1.06);}
  347. }
  348. @keyframes losePulse{
  349. 0%,100%{opacity:1;}
  350. 50%{opacity:.7;}
  351. }
  352. #quizResult{position:relative;z-index:10;}
  353. .hint{font-size:.8rem;color:var(--neon-pink);letter-spacing:1px;
  354. margin-top:8px;min-height:1.2em;}
  355. .track-slot{
  356. width:70px;height:70px;border-radius:12px;
  357. display:flex;align-items:center;justify-content:center;
  358. font-size:2rem;
  359. border:2px dashed rgba(0,229,255,.4);
  360. background:rgba(0,229,255,.05);
  361. color:rgba(200,220,255,.3);
  362. transition:.3s;
  363. }
  364. .track-slot.filled{
  365. border-style:solid;
  366. border-color:var(--neon-green);
  367. background:rgba(57,255,20,.1);
  368. box-shadow:0 0 20px rgba(57,255,20,.5);
  369. color:#fff;
  370. animation:pop .4s cubic-bezier(.2,1.4,.5,1);
  371. }
  372. #instrBtns .btn.active{
  373. background:var(--neon-pink);
  374. color:#000;
  375. box-shadow:0 0 25px var(--neon-pink);
  376. }
  377. .table{font-size:.85rem;color:#cfeaff;margin-top:8px;text-align:center;line-height:1.8;}
  378. .table span{color:var(--neon-cyan);font-weight:700;}
  379. </style>
  380. </head>
  381. <body>
  382. <div id="app">
  383. <div class="topbar">
  384. <span id="sceneLabel">СЦЕНА 1 / 7</span>
  385. <div class="progress"><div id="progressBar"></div></div>
  386. <button class="icon-btn" id="soundToggle" type="button">🔊 ЗВУК</button>
  387. </div>
  388. <!-- 🎆 canvas для спецэффектов -->
  389. <canvas id="fxCanvas"></canvas>
  390. <!-- SCENE 1 -->
  391. <section class="scene active" id="scene1">
  392. <canvas id="bgCanvas" width="900" height="500"
  393. style="position:absolute;top:0;left:0;width:100%;height:100%;opacity:.35;pointer-events:none;"></canvas>
  394. <div style="position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;">
  395. <h1>Механические волны</h1>
  396. <p>Интерактивный курс: колебания, звук и среды распространения.</p>
  397. <p class="hint">Наушники приветствуются 🎧</p>
  398. <div style="margin-top:24px;">
  399. <button class="btn" id="btnStart" type="button">▶ Начать курс</button>
  400. <button class="btn pink" id="btnWhat" type="button">Что такое волна?</button>
  401. </div>
  402. </div>
  403. </section>
  404. <!-- SCENE 2 -->
  405. <section class="scene" id="scene2">
  406. <h2>Что такое механическая волна?</h2>
  407. <p>Это распространение колебаний в упругой среде. Частицы колеблются, но не переносятся вместе с волной.</p>
  408. <div class="canvas-wrap"><canvas id="chainCanvas" width="800" height="200"></canvas></div>
  409. <div class="controls">
  410. <div class="slider-box">
  411. <label>Частота: <span id="freqVal">3</span> Гц</label>
  412. <input type="range" id="freqSlider" min="1" max="10" value="3">
  413. </div>
  414. <div class="slider-box">
  415. <label>Амплитуда: <span id="ampVal">25</span> px · 🔊 <span id="ampVol">21</span>%</label>
  416. <input type="range" id="ampSlider" min="5" max="40" value="25">
  417. </div>
  418. <button class="btn green" id="btnPush" type="button">Толкнуть</button>
  419. </div>
  420. <p class="hint" id="scene2Hint">Толчков: 0 / 3</p>
  421. <button class="btn" id="btnNext2" type="button" disabled>Далее →</button>
  422. </section>
  423. <!-- SCENE 3 -->
  424. <section class="scene" id="scene3">
  425. <h2>Среда распространения. Скорость звука</h2>
  426. <p>Чем плотнее среда — тем быстрее идёт звук.</p>
  427. <div class="tubes">
  428. <div class="tube air"><span class="label">🌬️ Воздух ≈ 343 м/с</span><div class="impulse" data-speed="2.4"></div></div>
  429. <div class="tube water"><span class="label">💧 Вода ≈ 1480 м/с</span><div class="impulse" data-speed="1.2"></div></div>
  430. <div class="tube steel"><span class="label">⚙️ Сталь ≈ 5100 м/с</span><div class="impulse" data-speed="0.5"></div></div>
  431. </div>
  432. <button class="btn" id="btnImpulse" type="button">Запустить импульс</button>
  433. <p class="question" style="margin-top:20px;">В какой среде звук распространяется быстрее всего?</p>
  434. <div class="quiz-options" id="quiz3">
  435. <button class="btn" data-ans="air" type="button">Воздух</button>
  436. <button class="btn" data-ans="water" type="button">Вода</button>
  437. <button class="btn" data-ans="steel" type="button">Сталь</button>
  438. </div>
  439. <button class="btn" id="btnNext3" type="button" disabled style="margin-top:20px;">Далее →</button>
  440. </section>
  441. <!-- SCENE 4 -->
  442. <section class="scene" id="scene4">
  443. <h2>Колебания: период, частота, амплитуда</h2>
  444. <div style="display:flex;flex-wrap:wrap;justify-content:center;gap:16px;">
  445. <div class="canvas-wrap"><canvas id="oscCanvas" width="360" height="260"></canvas></div>
  446. <div class="canvas-wrap"><canvas id="waveCanvas" width="360" height="260"></canvas></div>
  447. </div>
  448. <div class="controls">
  449. <div class="slider-box">
  450. <label>Амплитуда: <span id="ampVal2">60</span></label>
  451. <input type="range" id="ampSlider2" min="20" max="100" value="60">
  452. </div>
  453. <div class="slider-box">
  454. <label>Период: <span id="perVal">1.2</span> с</label>
  455. <input type="range" id="perSlider" min="4" max="30" value="12">
  456. </div>
  457. <button class="btn pink" id="btnCheck4" type="button">Проверить понимание</button>
  458. </div>
  459. <p class="table">Формулы: <span>T = 1/ν</span> · <span>ν = 1/T</span> · <span>v = λν</span></p>
  460. <button class="btn" id="btnNext4" type="button" disabled>Далее →</button>
  461. </section>
  462. <!-- SCENE 5 -->
  463. <section class="scene" id="scene5">
  464. <h2>Виды волн. Звук — продольная волна</h2>
  465. <div class="canvas-wrap">
  466. <canvas id="typeCanvas" width="800" height="320"></canvas>
  467. </div>
  468. <!-- 🎮 Мини-игра «Собери звуковую дорожку» -->
  469. <div id="waveGame" style="max-width:760px;text-align:center;">
  470. <p style="margin-bottom:8px;">
  471. 🎧 <b>Мини-игра:</b> нажми на каждый инструмент по очереди — собери <b>дорожку из 4 звуков</b>.
  472. Смотри, как меняется волна!
  473. </p>
  474. <div class="controls" id="instrBtns">
  475. <button class="btn" data-instr="piano" type="button">🎹 Пианино</button>
  476. <button class="btn" data-instr="drum" type="button">🥁 Барабан</button>
  477. <button class="btn" data-instr="whistle" type="button">🎺 Свисток</button>
  478. <button class="btn" data-instr="bass" type="button">🎸 Бас</button>
  479. </div>
  480. <div id="instrFreq" class="hint" style="font-size:.95rem;">
  481. 🎵 Нажми на инструмент, чтобы услышать и увидеть волну
  482. </div>
  483. <!-- Дорожка со слотами -->
  484. <div id="track" style="display:flex;gap:10px;justify-content:center;margin:16px 0;">
  485. <div class="track-slot" data-slot="0">—</div>
  486. <div class="track-slot" data-slot="1">—</div>
  487. <div class="track-slot" data-slot="2">—</div>
  488. <div class="track-slot" data-slot="3">—</div>
  489. </div>
  490. <p class="hint" id="scene5Hint">Собрано звуков: 0 / 4</p>
  491. </div>
  492. <!-- 💡 Пояснение (появляется после победы) -->
  493. <div id="waveFact" class="table" style="max-width:720px;display:none;">
  494. 🎓 <b>Разные звуки = разные волны.</b><br>
  495. 🔵 <b>Продольная</b> (сверху) — сжатия и разрежения. Так идёт звук в воздухе.<br>
  496. 🟢 <b>Поперечная</b> (снизу) — гребни и впадины. Так колеблется струна или волна на воде.<br>
  497. 🔊 Обе волны переносят <span>энергию</span>, а частицы среды <span>колеблются на месте</span>.
  498. </div>
  499. <button class="btn" id="btnNext5" type="button" disabled>Далее →</button>
  500. </section>
  501. <!-- SCENE 6 -->
  502. <section class="scene" id="scene6">
  503. <h2>Применение звуковых волн</h2>
  504. <p>Нажми на карточку, чтобы узнать подробнее.</p>
  505. <div class="cards" id="cards">
  506. <div class="card eco"><div class="card-inner">
  507. <div class="card-face">
  508. <img class="card-img"
  509. src=![616554.png](http://)
  510. alt="Эхолокация"
  511. onerror="this.replaceWith(Object.assign(document.createElement('div'),{textContent:'🦇',style:'font-size:3.4rem;line-height:1;'}));" />
  512. <div class="card-title">Эхолокация</div>
  513. </div>
  514. <div class="card-face back">
  515. <img class="card-img"
  516. src=[616554.png](http://)
  517. alt="Эхолокация"
  518. onerror="this.replaceWith(Object.assign(document.createElement('div'),{textContent:'🦇',style:'font-size:1.4rem;line-height:1;'}));" />
  519. <div>Летучие мыши и дельфины определяют препятствия по отражённому ультразвуку.</div>
  520. </div>
  521. </div></div>
  522. <div class="card uzi"><div class="card-inner">
  523. <div class="card-face">
  524. <img class="card-img"
  525. src=[2966327.png](http://)
  526. alt="УЗИ"
  527. onerror="this.replaceWith(Object.assign(document.createElement('div'),{textContent:'🩻',style:'font-size:3.4rem;line-height:1;'}));" />
  528. <div class="card-title">УЗИ</div>
  529. </div>
  530. <div class="card-face back">
  531. <img class="card-img"
  532. src=[2966327.png](http://)
  533. alt="УЗИ"
  534. onerror="this.replaceWith(Object.assign(document.createElement('div'),{textContent:'🩻',style:'font-size:1.4rem;line-height:1;'}));" />
  535. <div>Медицинская визуализация: ультразвук отражается от границ тканей.</div>
  536. </div>
  537. </div></div>
  538. <div class="card seismo"><div class="card-inner">
  539. <div class="card-face">
  540. <img class="card-img"
  541. src=[2909760.png](http://)
  542. alt="Сейсмология"
  543. onerror="this.replaceWith(Object.assign(document.createElement('div'),{textContent:'🌍',style:'font-size:3.4rem;line-height:1;'}));" />
  544. <div class="card-title">Сейсмология</div>
  545. </div>
  546. <div class="card-face back">
  547. <img class="card-img"
  548. src=[2909760.png](http://)
  549. alt="Сейсмология"
  550. onerror="this.replaceWith(Object.assign(document.createElement('div'),{textContent:'🌍',style:'font-size:1.4rem;line-height:1;'}));" />
  551. <div>По сейсмическим волнам изучают строение Земли и землетрясения.</div>
  552. </div>
  553. </div></div>
  554. <div class="card music"><div class="card-inner">
  555. <div class="card-face">
  556. <img class="card-img"
  557. src=[384281.png](http://)
  558. alt="Музыка"
  559. onerror="this.replaceWith(Object.assign(document.createElement('div'),{textContent:'🎸',style:'font-size:3.4rem;line-height:1;'}));" />
  560. <div class="card-title">Музыка</div>
  561. </div>
  562. <div class="card-face back">
  563. <img class="card-img"
  564. src=[384281.png](http://)
  565. alt="Музыка"
  566. onerror="this.replaceWith(Object.assign(document.createElement('div'),{textContent:'🎸',style:'font-size:1.4rem;line-height:1;'}));" />
  567. <div>Струны и столбы воздуха создают стоячие волны — ноты.</div>
  568. </div>
  569. </div></div>
  570. </div>
  571. <p>Собери пару: пример ↔ физическое явление.</p>
  572. <div class="pairs">
  573. <div class="pair-col" id="pairLeft"></div>
  574. <div class="pair-col" id="pairRight"></div>
  575. </div>
  576. <p class="hint" id="scene6Hint">Собрано пар: 0 / 4</p>
  577. <button class="btn" id="btnNext6" type="button" disabled>К тестированию →</button>
  578. </section>
  579. <!-- SCENE 7 -->
  580. <section class="scene" id="scene7">
  581. <h2 id="quizTitle">Итоговое тестирование</h2>
  582. <div class="progress-quiz"><div id="quizProgress"></div></div>
  583. <p class="question" id="quizQuestion">…</p>
  584. <div class="quiz-options" id="quizOptions"></div>
  585. <div id="quizResult" style="display:none;flex-direction:column;align-items:center;">
  586. <div class="final-badge" id="finalBadge">…</div>
  587. <div class="result-score" id="finalScore">0</div>
  588. <p id="finalText" style="margin:16px 0;"></p>
  589. <button class="btn pink" id="btnRestart" type="button">Пройти заново</button>
  590. </div>
  591. </section>
  592. <!-- MODAL -->
  593. <div class="modal" id="modal">
  594. <div class="modal-content">
  595. <h2 id="modalTitle">…</h2>
  596. <p id="modalText" style="margin:16px 0;">…</p>
  597. <button class="btn" id="modalClose" type="button">Закрыть</button>
  598. </div>
  599. </div>
  600. </div>
  601. <script>
  602. /* ================= ЯДРО: НАВИГАЦИЯ + ЗВУК + МОДАЛКА ================= */
  603. var AudioCtx = window.AudioContext || window.webkitAudioContext;
  604. var audioCtx = null;
  605. var soundOn = true;
  606. function ensureAudio(){
  607. try{
  608. if(!audioCtx) audioCtx = new AudioCtx();
  609. if(audioCtx.state === 'suspended') audioCtx.resume();
  610. }catch(e){}
  611. }
  612. function beep(freq, dur, type, vol){
  613. if(!soundOn || !audioCtx) return;
  614. try{
  615. var o = audioCtx.createOscillator();
  616. var g = audioCtx.createGain();
  617. o.type = type || 'sine';
  618. o.frequency.value = freq || 440;
  619. g.gain.setValueAtTime(vol || 0.12, audioCtx.currentTime);
  620. g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + (dur || 0.15));
  621. o.connect(g); g.connect(audioCtx.destination);
  622. o.start(); o.stop(audioCtx.currentTime + (dur || 0.15));
  623. }catch(e){}
  624. }
  625. function correctSound(){ beep(880,0.12,'triangle',0.18); setTimeout(function(){ beep(1320,0.15,'triangle',0.18); },110); }
  626. function wrongSound(){ beep(220,0.25,'sawtooth',0.15); }
  627. function clickSound(){ beep(600,0.06,'square',0.08); }
  628. var scenes = ['scene1','scene2','scene3','scene4','scene5','scene6','scene7'];
  629. var currentScene = 0;
  630. function goToScene(idx){
  631. if(idx < 0 || idx >= scenes.length) return;
  632. var all = document.querySelectorAll('.scene');
  633. for(var i=0;i<all.length;i++) all[i].classList.remove('active');
  634. document.getElementById(scenes[idx]).classList.add('active');
  635. currentScene = idx;
  636. document.getElementById('sceneLabel').textContent = 'СЦЕНА ' + (idx+1) + ' / ' + scenes.length;
  637. document.getElementById('progressBar').style.width = (idx/(scenes.length-1)*100) + '%';
  638. clickSound();
  639. if(idx === 3 && typeof startOscillators === 'function') startOscillators();
  640. if(idx === 4 && typeof initMolecules === 'function') initMolecules();
  641. if(idx === 5 && typeof initPairs === 'function') initPairs();
  642. if(idx === 6 && typeof startQuiz === 'function') startQuiz();
  643. }
  644. var modal = document.getElementById('modal');
  645. function openModal(title, text){
  646. document.getElementById('modalTitle').textContent = title;
  647. document.getElementById('modalText').textContent = text;
  648. modal.classList.add('active');
  649. }
  650. document.getElementById('modalClose').addEventListener('click', function(){
  651. modal.classList.remove('active'); clickSound();
  652. });
  653. modal.addEventListener('click', function(e){ if(e.target === modal) modal.classList.remove('active'); });
  654. document.getElementById('soundToggle').addEventListener('click', function(e){
  655. soundOn = !soundOn;
  656. e.target.textContent = soundOn ? '🔊 ЗВУК' : '🔇 ВЫКЛ';
  657. if(soundOn){ ensureAudio(); clickSound(); }
  658. });
  659. /* ================= ФОН ================= */
  660. var bgCanvas = document.getElementById('bgCanvas');
  661. var bgCtx = bgCanvas.getContext('2d');
  662. var bgT = 0;
  663. function drawBg(){
  664. var W = bgCanvas.width, H = bgCanvas.height;
  665. bgCtx.clearRect(0,0,W,H);
  666. for(var k=0;k<3;k++){
  667. bgCtx.beginPath();
  668. bgCtx.strokeStyle = k===0 ? 'rgba(255,46,196,.5)' : k===1 ? 'rgba(0,229,255,.5)' : 'rgba(162,89,255,.5)';
  669. bgCtx.lineWidth = 2;
  670. bgCtx.shadowBlur = 20;
  671. bgCtx.shadowColor = bgCtx.strokeStyle;
  672. for(var x=0;x<W;x+=4){
  673. var y = H/2 + Math.sin(x*0.02 + bgT + k*1.3) * (60 + k*20);
  674. if(x===0) bgCtx.moveTo(x,y); else bgCtx.lineTo(x,y);
  675. }
  676. bgCtx.stroke();
  677. }
  678. bgCtx.shadowBlur = 0;
  679. bgT += 0.03;
  680. requestAnimationFrame(drawBg);
  681. }
  682. drawBg();
  683. /* ================= ГЛАВНАЯ: КНОПКИ ================= */
  684. document.getElementById('btnStart').addEventListener('click', function(){
  685. ensureAudio();
  686. clickSound();
  687. goToScene(1); // ← ПЕРЕХОД НА СЦЕНУ 2 (начало курса)
  688. });
  689. document.getElementById('btnWhat').addEventListener('click', function(){
  690. ensureAudio();
  691. clickSound();
  692. openModal(
  693. 'Что такое волна?',
  694. 'Механическая волна — это процесс распространения колебаний в упругой среде. Частицы среды колеблются около положения равновесия, а сама волна переносит энергию, но не вещество. Примеры: звук в воздухе, волны на воде, сейсмические волны в Земле.'
  695. );
  696. });
  697. /* ================= SCENE 2: цепочка ================= */
  698. (function(){
  699. var c = document.getElementById('chainCanvas');
  700. var ctx = c.getContext('2d');
  701. var t = 0, amp = 25, freq = 3, pushTime = -999, pushes = 0;
  702. // 🔊 Обновление звука при движении бегунка частоты:
  703. // 1 Гц → 440 Гц, 10 Гц → 4400 Гц (линейно: 440 × freq)
  704. function playFreqTone(){
  705. if(!soundOn) return;
  706. ensureAudio();
  707. if(!audioCtx) return;
  708. // останавливаем предыдущий тон
  709. if(freqTone){
  710. try{ freqTone.stop(); }catch(e){}
  711. freqTone = null;
  712. }
  713. var soundFreq = 440 * freq;
  714. var vol = ampToVolume(); // 🔊 громкость по амплитуде
  715. var o = audioCtx.createOscillator();
  716. var g = audioCtx.createGain();
  717. o.type = 'sine';
  718. o.frequency.value = soundFreq;
  719. var now = audioCtx.currentTime;
  720. g.gain.setValueAtTime(0, now);
  721. g.gain.linearRampToValueAtTime(vol, now + 0.02);
  722. g.gain.setValueAtTime(vol, now + 0.20);
  723. g.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
  724. o.connect(g); g.connect(audioCtx.destination);
  725. o.start(now);
  726. o.stop(now + 0.4);
  727. freqTone = o;
  728. }
  729. document.getElementById('freqSlider').addEventListener('input', function(e){
  730. freq = +e.target.value;
  731. document.getElementById('freqVal').textContent = freq;
  732. document.getElementById('freqSound').textContent = 440 * freq;
  733. playFreqTone();
  734. });
  735. // 🔊 Громкость зависит от амплитуды: 5 px → 0.02, 40 px → 0.35
  736. function ampToVolume(){
  737. var minA = 5, maxA = 40;
  738. var minV = 0.02, maxV = 0.35;
  739. var k = (amp - minA) / (maxA - minA);
  740. if(k < 0) k = 0;
  741. if(k > 1) k = 1;
  742. return minV + k * (maxV - minV);
  743. }
  744. document.getElementById('ampSlider').addEventListener('input', function(e){
  745. amp = +e.target.value;
  746. document.getElementById('ampVal').textContent = amp;
  747. document.getElementById('ampVol').textContent = Math.round(ampToVolume() / 0.35 * 100);
  748. // 🔊 короткий пробный тон с новой громкостью
  749. if(soundOn){
  750. ensureAudio();
  751. beep(440 * freq, 0.25, 'sine', ampToVolume());
  752. }
  753. });
  754. document.getElementById('btnPush').addEventListener('click', function(){
  755. pushTime = t; pushes++;
  756. document.getElementById('scene2Hint').textContent = 'Толчков: ' + pushes + ' / 3';
  757. // 🔊 Частота — по бегунку частоты, громкость — по бегунку амплитуды
  758. beep(440 * freq, 0.35, 'sine', ampToVolume());
  759. if(pushes >= 3){
  760. document.getElementById('btnNext2').disabled = false;
  761. document.getElementById('scene2Hint').textContent = 'Отлично! Можно идти дальше.';
  762. }
  763. });
  764. document.getElementById('btnNext2').addEventListener('click', function(){ goToScene(2); });
  765. function loop(){
  766. var W = c.width, H = c.height;
  767. ctx.clearRect(0,0,W,H);
  768. var N = 12, spacing = W/(N+1);
  769. t += 0.03;
  770. var pts = [];
  771. for(var i=0;i<N;i++){
  772. var x = spacing*(i+1);
  773. var delay = i*0.25/Math.max(freq,1);
  774. var dt = t - pushTime - delay;
  775. var off;
  776. if(dt > 0){
  777. off = Math.sin(dt*freq*2*Math.PI) * amp * Math.exp(-dt*1.2);
  778. } else {
  779. off = Math.sin(t*freq*0.5) * amp * 0.15;
  780. }
  781. pts.push({x:x, y:H/2 + off});
  782. }
  783. ctx.strokeStyle = 'rgba(0,229,255,.5)';
  784. ctx.lineWidth = 2;
  785. ctx.shadowBlur = 10; ctx.shadowColor = '#00e5ff';
  786. for(var j=0;j<pts.length-1;j++){
  787. ctx.beginPath();
  788. ctx.moveTo(pts[j].x, pts[j].y);
  789. ctx.lineTo(pts[j+1].x, pts[j+1].y);
  790. ctx.stroke();
  791. }
  792. for(var k=0;k<pts.length;k++){
  793. ctx.beginPath();
  794. ctx.fillStyle = '#ff2ec4';
  795. ctx.shadowBlur = 20; ctx.shadowColor = '#ff2ec4';
  796. ctx.arc(pts[k].x, pts[k].y, 10, 0, Math.PI*2);
  797. ctx.fill();
  798. }
  799. ctx.shadowBlur = 0;
  800. requestAnimationFrame(loop);
  801. }
  802. loop();
  803. })();
  804. /* ================= SCENE 3: трубы + квиз ================= */
  805. document.getElementById('btnImpulse').addEventListener('click', function(){
  806. clickSound();
  807. var imps = document.querySelectorAll('#scene3 .impulse');
  808. for(var i=0;i<imps.length;i++){
  809. imps[i].style.animationDuration = imps[i].dataset.speed + 's';
  810. imps[i].classList.remove('run');
  811. void imps[i].offsetWidth;
  812. imps[i].classList.add('run');
  813. }
  814. });
  815. var q3 = document.querySelectorAll('#quiz3 .btn');
  816. for(var qi=0; qi<q3.length; qi++){
  817. (function(b){
  818. b.addEventListener('click', function(){
  819. if(b.dataset.ans === 'steel'){
  820. b.classList.add('correct'); correctSound();
  821. document.getElementById('btnNext3').disabled = false;
  822. } else {
  823. b.classList.add('wrong'); wrongSound();
  824. }
  825. });
  826. })(q3[qi]);
  827. }
  828. document.getElementById('btnNext3').addEventListener('click', function(){ goToScene(3); });
  829. /* ================= SCENE 4: маятник + волна ================= */
  830. var oscCanvas = document.getElementById('oscCanvas');
  831. var oscCtx = oscCanvas.getContext('2d');
  832. var waveCanvas = document.getElementById('waveCanvas');
  833. var waveCtx = waveCanvas.getContext('2d');
  834. var oscT = 0, oscAmp = 60, oscPeriod = 1.2;
  835. var oscRunning = false;
  836. document.getElementById('ampSlider2').addEventListener('input', function(e){
  837. oscAmp = +e.target.value;
  838. document.getElementById('ampVal2').textContent = oscAmp;
  839. });
  840. document.getElementById('perSlider').addEventListener('input', function(e){
  841. oscPeriod = +e.target.value / 10;
  842. document.getElementById('perVal').textContent = oscPeriod.toFixed(1);
  843. });
  844. function startOscillators(){
  845. if(oscRunning) return;
  846. oscRunning = true;
  847. oscT = 0;
  848. requestAnimationFrame(drawOsc);
  849. }
  850. function drawOsc(){
  851. // ⏱️ Шаг времени. Период управляет скоростью фазы.
  852. var dt = 0.03;
  853. oscT += dt;
  854. var W = oscCanvas.width, H = oscCanvas.height;
  855. oscCtx.clearRect(0,0,W,H);
  856. // ================= ЛЕВЫЙ CANVAS: МАЯТНИК =================
  857. // Ось подвеса
  858. oscCtx.strokeStyle = 'rgba(0,229,255,.3)';
  859. oscCtx.lineWidth = 1;
  860. oscCtx.beginPath(); oscCtx.moveTo(W/2, 30); oscCtx.lineTo(W/2, H-30); oscCtx.stroke();
  861. // 🕒 Период управляет скоростью качания (фаза)
  862. var phase = (2 * Math.PI / oscPeriod) * oscT;
  863. // 🎚️ Амплитуда управляет ШИРИНОЙ колебания (углом отклонения)
  864. // oscAmp: 20..100 → maxAngle: 0.08..0.55 рад (≈5°…32°)
  865. var maxAngle = (oscAmp / 100) * 0.55;
  866. var angle = Math.sin(phase) * maxAngle;
  867. var len = 120, cx = W/2, cy = 40;
  868. var bx = cx + Math.sin(angle) * len;
  869. var by = cy + Math.cos(angle) * len;
  870. // Нить
  871. oscCtx.strokeStyle = '#a259ff';
  872. oscCtx.lineWidth = 2;
  873. oscCtx.shadowBlur = 12; oscCtx.shadowColor = '#a259ff';
  874. oscCtx.beginPath(); oscCtx.moveTo(cx, cy); oscCtx.lineTo(bx, by); oscCtx.stroke();
  875. // Груз
  876. oscCtx.beginPath();
  877. oscCtx.fillStyle = '#ff2ec4';
  878. oscCtx.shadowBlur = 25; oscCtx.shadowColor = '#ff2ec4';
  879. oscCtx.arc(bx, by, 14, 0, Math.PI*2);
  880. oscCtx.fill();
  881. oscCtx.shadowBlur = 0;
  882. // Пунктирные «границы» размаха — показывают, где амплитуда
  883. oscCtx.strokeStyle = 'rgba(255,46,196,.25)';
  884. oscCtx.setLineDash([4,4]);
  885. oscCtx.lineWidth = 1;
  886. var leftX = cx + Math.sin(-maxAngle) * len;
  887. var rightX = cx + Math.sin( maxAngle) * len;
  888. oscCtx.beginPath();
  889. oscCtx.moveTo(leftX, 30); oscCtx.lineTo(leftX, H-30);
  890. oscCtx.moveTo(rightX, 30); oscCtx.lineTo(rightX, H-30);
  891. oscCtx.stroke();
  892. oscCtx.setLineDash([]);
  893. // ================= ПРАВЫЙ CANVAS: ВОЛНА =================
  894. var WW = waveCanvas.width, WH = waveCanvas.height;
  895. waveCtx.clearRect(0,0,WW,WH);
  896. // Нулевая линия
  897. waveCtx.strokeStyle = 'rgba(0,229,255,.2)';
  898. waveCtx.lineWidth = 1;
  899. waveCtx.beginPath(); waveCtx.moveTo(0, WH/2); waveCtx.lineTo(WW, WH/2); waveCtx.stroke();
  900. // 🎚️ Амплитуда = ВЫСОТА волны (размах по вертикали)
  901. // oscAmp: 20..100 → pixelAmp: 20..110 px (не выходит за пределы холста)
  902. var pixelAmp = oscAmp * 1.1;
  903. // Пунктирные границы размаха — видно, как амплитуда расширяется/сужается
  904. waveCtx.strokeStyle = 'rgba(255,46,196,.25)';
  905. waveCtx.setLineDash([4,4]);
  906. waveCtx.lineWidth = 1;
  907. waveCtx.beginPath();
  908. waveCtx.moveTo(0, WH/2 - pixelAmp); waveCtx.lineTo(WW, WH/2 - pixelAmp);
  909. waveCtx.moveTo(0, WH/2 + pixelAmp); waveCtx.lineTo(WW, WH/2 + pixelAmp);
  910. waveCtx.stroke();
  911. waveCtx.setLineDash([]);
  912. // 🕒 Период = скорость бега волны
  913. // 🎚️ Амплитуда = высота (размах)
  914. waveCtx.strokeStyle = '#00e5ff';
  915. waveCtx.lineWidth = 2;
  916. waveCtx.shadowBlur = 15; waveCtx.shadowColor = '#00e5ff';
  917. waveCtx.beginPath();
  918. for(var x=0; x<WW; x++){
  919. var phaseX = (x / WW) * Math.PI * 4 + (2 * Math.PI / oscPeriod) * oscT;
  920. var y = WH/2 + Math.sin(phaseX) * pixelAmp; // ← размах = амплитуда
  921. if(x===0) waveCtx.moveTo(x,y); else waveCtx.lineTo(x,y);
  922. }
  923. waveCtx.stroke();
  924. waveCtx.shadowBlur = 0;
  925. requestAnimationFrame(drawOsc);
  926. }
  927. document.getElementById('btnCheck4').addEventListener('click', function(){
  928. ensureAudio();
  929. openModal(
  930. 'Проверь себя',
  931. 'Если период T = 2 с, то частота ν = 1/T = 0,5 Гц. А если частота ν = 4 Гц, то период T = 1/ν = 0,25 с. Запомни: T и ν — взаимно обратные величины.'
  932. );
  933. document.getElementById('btnNext4').disabled = false;
  934. correctSound();
  935. });
  936. document.getElementById('btnNext4').addEventListener('click', function(){ goToScene(4); });
  937. /* ================= SCENE 5: две волны одновременно + мини-игра ================= */
  938. var typeCanvas = document.getElementById('typeCanvas');
  939. var typeCtx = typeCanvas.getContext('2d');
  940. var typeT = 0;
  941. // 🎼 Инструменты
  942. var INSTRUMENTS = {
  943. piano: { name:'Пианино', emoji:'🎹', freq:261.63, wave:'sine', color:'#00e5ff', desc:'261 Гц · До первой октавы' },
  944. drum: { name:'Барабан', emoji:'🥁', freq:80, wave:'noise', color:'#ff2ec4', desc:'~80 Гц · шумный удар' },
  945. whistle: { name:'Свисток', emoji:'🎺', freq:1046.50, wave:'sine', color:'#39ff14', desc:'1046 Гц · До второй октавы (высокий)' },
  946. bass: { name:'Бас', emoji:'🎸', freq:130.81, wave:'square', color:'#a259ff', desc:'130 Гц · До малой октавы (низкий)' }
  947. };
  948. var track = [];
  949. var currentInstr = null;
  950. var gameWin = false;
  951. // 🔘 Клики по инструментам
  952. var instrBtns = document.querySelectorAll('#instrBtns .btn');
  953. for(var ib=0; ib<instrBtns.length; ib++){
  954. (function(btn){
  955. btn.addEventListener('click', function(){
  956. var key = btn.dataset.instr;
  957. var instr = INSTRUMENTS[key];
  958. if(!instr) return;
  959. // Уже нажат — просто заменим волну, не добавляем в дорожку повторно
  960. var alreadyIn = track.indexOf(key) !== -1;
  961. // 🌊 Всегда переключаем текущий инструмент — волна меняется сразу
  962. currentInstr = key;
  963. // 📥 Добавляем в дорожку ТОЛЬКО если ещё не было
  964. if(!alreadyIn){
  965. track.push(key);
  966. var slot = document.querySelector('.track-slot[data-slot="' + (track.length - 1) + '"]');
  967. if(slot){
  968. slot.textContent = instr.emoji;
  969. slot.classList.add('filled');
  970. }
  971. }
  972. // 🎨 Подсветка кнопки
  973. for(var k=0;k<instrBtns.length;k++) instrBtns[k].classList.remove('active');
  974. btn.classList.add('active');
  975. // 🔊 Звук играем всегда (даже при повторном клике)
  976. playInstrumentSound(key);
  977. // 📝 Инфо-подпись
  978. document.getElementById('instrFreq').textContent =
  979. instr.emoji + ' ' + instr.name + ' — ' + instr.desc;
  980. document.getElementById('scene5Hint').textContent =
  981. 'Собрано звуков: ' + track.length + ' / 4';
  982. // 🎉 Победа — только один раз
  983. if(track.length >= 4 && !gameWin){
  984. gameWin = true;
  985. setTimeout(function(){
  986. document.getElementById('scene5Hint').textContent =
  987. '🎉 Мелодия собрана! Меняй инструменты — волна меняется!';
  988. document.getElementById('waveFact').style.display = 'block';
  989. document.getElementById('btnNext5').disabled = false;
  990. playMelody();
  991. }, 400);
  992. }
  993. });
  994. })(instrBtns[ib]);
  995. }
  996. // 🔊 Синтез звука инструмента
  997. function playInstrumentSound(key){
  998. if(!soundOn) return;
  999. ensureAudio();
  1000. if(!audioCtx) return;
  1001. var instr = INSTRUMENTS[key];
  1002. if(instr.wave === 'noise'){
  1003. var bufferSize = audioCtx.sampleRate * 0.2;
  1004. var buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
  1005. var data = buffer.getChannelData(0);
  1006. for(var i=0;i<bufferSize;i++) data[i] = (Math.random()*2 - 1);
  1007. var src = audioCtx.createBufferSource();
  1008. src.buffer = buffer;
  1009. var g = audioCtx.createGain();
  1010. g.gain.setValueAtTime(0.25, audioCtx.currentTime);
  1011. g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.2);
  1012. src.connect(g); g.connect(audioCtx.destination);
  1013. src.start();
  1014. } else {
  1015. var o = audioCtx.createOscillator();
  1016. var g2 = audioCtx.createGain();
  1017. o.type = instr.wave;
  1018. o.frequency.value = instr.freq;
  1019. g2.gain.setValueAtTime(0, audioCtx.currentTime);
  1020. g2.gain.linearRampToValueAtTime(0.2, audioCtx.currentTime + 0.03);
  1021. g2.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.7);
  1022. o.connect(g2); g2.connect(audioCtx.destination);
  1023. o.start();
  1024. o.stop(audioCtx.currentTime + 0.8);
  1025. }
  1026. }
  1027. // 🎵 Победная мелодия
  1028. function playMelody(){
  1029. if(!soundOn) return;
  1030. ensureAudio();
  1031. if(!audioCtx) return;
  1032. var seq = track.map(function(k){ return INSTRUMENTS[k].freq; });
  1033. seq.forEach(function(f, i){
  1034. setTimeout(function(){
  1035. var o = audioCtx.createOscillator();
  1036. var g = audioCtx.createGain();
  1037. o.type = 'triangle';
  1038. o.frequency.value = f;
  1039. g.gain.setValueAtTime(0, audioCtx.currentTime);
  1040. g.gain.linearRampToValueAtTime(0.2, audioCtx.currentTime + 0.02);
  1041. g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5);
  1042. o.connect(g); g.connect(audioCtx.destination);
  1043. o.start();
  1044. o.stop(audioCtx.currentTime + 0.55);
  1045. }, i * 220);
  1046. });
  1047. }
  1048. // 🔁 Основной цикл — РИСУЕМ ОБЕ ВОЛНЫ
  1049. function drawType(){
  1050. var W = typeCanvas.width, H = typeCanvas.height;
  1051. typeCtx.clearRect(0,0,W,H);
  1052. typeT += 0.04;
  1053. // ============== ВЕРХНЯЯ ПОЛОВИНА: ПРОДОЛЬНАЯ ==============
  1054. var rowLong = H * 0.30; // центр продольной волны (30% высоты)
  1055. drawLongitudinal(W, rowLong);
  1056. // Разделитель
  1057. typeCtx.strokeStyle = 'rgba(255,255,255,.08)';
  1058. typeCtx.setLineDash([6,8]);
  1059. typeCtx.lineWidth = 1;
  1060. typeCtx.beginPath();
  1061. typeCtx.moveTo(20, H / 2);
  1062. typeCtx.lineTo(W - 20, H / 2);
  1063. typeCtx.stroke();
  1064. typeCtx.setLineDash([]);
  1065. // ============== НИЖНЯЯ ПОЛОВИНА: ПОПЕРЕЧНАЯ ==============
  1066. var rowTrans = H * 0.72; // центр поперечной волны (72% высоты)
  1067. drawTransverse(W, rowTrans);
  1068. // Общие иконки "источник → ухо"
  1069. typeCtx.font = 'bold 18px Segoe UI';
  1070. typeCtx.fillStyle = 'rgba(57,255,20,.85)';
  1071. typeCtx.fillText('🔊', 4, rowLong + 6);
  1072. typeCtx.fillText('🔊', 4, rowTrans + 6);
  1073. typeCtx.fillStyle = 'rgba(0,229,255,.85)';
  1074. typeCtx.fillText('👂', W - 28, rowLong + 6);
  1075. typeCtx.fillText('👂', W - 28, rowTrans + 6);
  1076. requestAnimationFrame(drawType);
  1077. }
  1078. // 🌊 ВЕРХНЯЯ: продольная волна — сжатия/разрежения
  1079. function drawLongitudinal(W, y0){
  1080. var N = 55;
  1081. var spacing = W / (N + 1);
  1082. // Фоновый "коридор"
  1083. typeCtx.fillStyle = 'rgba(255,46,196,.04)';
  1084. typeCtx.fillRect(30, y0 - 30, W - 60, 60);
  1085. for(var i=0;i<N;i++){
  1086. var baseX = spacing * (i + 1);
  1087. var k = (2 * Math.PI * 4) / N;
  1088. var shift = Math.sin(typeT - i * k) * 11;
  1089. var x = baseX + shift;
  1090. var density = 0.5 + 0.5 * Math.sin(typeT - i * k);
  1091. var r = 3.5 + density * 3.5;
  1092. var alpha = 0.35 + density * 0.65;
  1093. typeCtx.beginPath();
  1094. typeCtx.fillStyle = 'rgba(255,46,196,' + alpha + ')';
  1095. typeCtx.shadowBlur = 10; typeCtx.shadowColor = '#ff2ec4';
  1096. typeCtx.arc(x, y0, r, 0, Math.PI*2);
  1097. typeCtx.fill();
  1098. }
  1099. typeCtx.shadowBlur = 0;
  1100. // Подпись
  1101. typeCtx.fillStyle = '#ff2ec4';
  1102. typeCtx.font = 'bold 13px Segoe UI';
  1103. typeCtx.fillText('🔴 Продольная (звук в воздухе)', 50, y0 - 42);
  1104. }
  1105. // 🌊 НИЖНЯЯ: поперечная волна — синусоида (или волна выбранного инструмента)
  1106. function drawTransverse(W, y0){
  1107. // Ось
  1108. typeCtx.strokeStyle = 'rgba(0,229,255,.12)';
  1109. typeCtx.lineWidth = 1;
  1110. typeCtx.beginPath();
  1111. typeCtx.moveTo(30, y0);
  1112. typeCtx.lineTo(W - 30, y0);
  1113. typeCtx.stroke();
  1114. var amp = 40;
  1115. // Если выбран инструмент — рисуем ЕГО волну
  1116. if(currentInstr && INSTRUMENTS[currentInstr]){
  1117. var instr = INSTRUMENTS[currentInstr];
  1118. typeCtx.strokeStyle = instr.color;
  1119. typeCtx.lineWidth = 2.5;
  1120. typeCtx.shadowBlur = 18; typeCtx.shadowColor = instr.color;
  1121. typeCtx.beginPath();
  1122. for(var x=30; x<W-30; x+=3){
  1123. var phase = (x / W) * Math.PI * 2 * 5 + typeT * 2;
  1124. var y;
  1125. if(instr.wave === 'sine'){
  1126. y = y0 + Math.sin(phase) * amp;
  1127. } else if(instr.wave === 'square'){
  1128. y = y0 + Math.sign(Math.sin(phase)) * amp * 0.8;
  1129. } else {
  1130. y = y0 + (Math.random()*2 - 1) * amp * 0.85;
  1131. }
  1132. if(x===30) typeCtx.moveTo(x,y); else typeCtx.lineTo(x,y);
  1133. }
  1134. typeCtx.stroke();
  1135. typeCtx.shadowBlur = 0;
  1136. typeCtx.fillStyle = instr.color;
  1137. typeCtx.font = 'bold 13px Segoe UI';
  1138. typeCtx.fillText(instr.emoji + ' ' + instr.name + ' · ' + instr.freq + ' Гц', 50, y0 - 42);
  1139. } else {
  1140. // Обычная синусоида
  1141. typeCtx.strokeStyle = '#00e5ff';
  1142. typeCtx.lineWidth = 2;
  1143. typeCtx.shadowBlur = 15; typeCtx.shadowColor = '#00e5ff';
  1144. typeCtx.beginPath();
  1145. for(var x2=30; x2<W-30; x2+=3){
  1146. var y2 = y0 + Math.sin((x2/W)*Math.PI*2*5 + typeT * 2) * amp;
  1147. if(x2===30) typeCtx.moveTo(x2,y2); else typeCtx.lineTo(x2,y2);
  1148. }
  1149. typeCtx.stroke();
  1150. typeCtx.shadowBlur = 0;
  1151. typeCtx.fillStyle = '#00e5ff';
  1152. typeCtx.font = 'bold 13px Segoe UI';
  1153. typeCtx.fillText('🟢 Поперечная (струна, волна на воде)', 50, y0 - 42);
  1154. }
  1155. }
  1156. // ▶️ Далее
  1157. document.getElementById('btnNext5').addEventListener('click', function(){ goToScene(5); });
  1158. // Запуск
  1159. drawType();
  1160. /* ================= SCENE 6: карточки + пары ================= */
  1161. var pairData = [
  1162. { left: 'Эхолокация', right: 'Отражение ультразвука' },
  1163. { left: 'УЗИ', right: 'Отражение от тканей' },
  1164. { left: 'Сейсмология', right: 'Сейсмические волны' },
  1165. { left: 'Музыка', right: 'Стоячие волны' }
  1166. ];
  1167. var pairsDone = 0;
  1168. var selectedLeft = null;
  1169. function initPairs(){
  1170. pairsDone = 0;
  1171. selectedLeft = null;
  1172. document.getElementById('scene6Hint').textContent = 'Собрано пар: 0 / 4';
  1173. document.getElementById('btnNext6').disabled = true;
  1174. var leftCol = document.getElementById('pairLeft');
  1175. var rightCol = document.getElementById('pairRight');
  1176. leftCol.innerHTML = ''; rightCol.innerHTML = '';
  1177. // левая колонка по порядку
  1178. pairData.forEach(function(p, i){
  1179. var d = document.createElement('div');
  1180. d.className = 'pair-item';
  1181. d.textContent = p.left;
  1182. d.dataset.idx = i;
  1183. d.dataset.side = 'left';
  1184. d.addEventListener('click', function(){
  1185. if(d.classList.contains('matched')) return;
  1186. var sel = document.querySelectorAll('#pairLeft .pair-item.selected');
  1187. for(var s=0;s<sel.length;s++) sel[s].classList.remove('selected');
  1188. d.classList.add('selected');
  1189. selectedLeft = d;
  1190. clickSound();
  1191. });
  1192. leftCol.appendChild(d);
  1193. });
  1194. // правая колонка перемешана
  1195. var shuffled = pairData.map(function(p,i){ return {text:p.right, idx:i}; });
  1196. shuffled.sort(function(){ return Math.random()-0.5; });
  1197. shuffled.forEach(function(item){
  1198. var d = document.createElement('div');
  1199. d.className = 'pair-item';
  1200. d.textContent = item.text;
  1201. d.dataset.idx = item.idx;
  1202. d.dataset.side = 'right';
  1203. d.addEventListener('click', function(){
  1204. if(d.classList.contains('matched')) return;
  1205. if(!selectedLeft){ wrongSound(); return; }
  1206. if(selectedLeft.dataset.idx === d.dataset.idx){
  1207. selectedLeft.classList.add('matched');
  1208. selectedLeft.classList.remove('selected');
  1209. d.classList.add('matched');
  1210. selectedLeft = null;
  1211. pairsDone++;
  1212. correctSound();
  1213. document.getElementById('scene6Hint').textContent = 'Собрано пар: ' + pairsDone + ' / 4';
  1214. if(pairsDone >= 4){
  1215. document.getElementById('btnNext6').disabled = false;
  1216. document.getElementById('scene6Hint').textContent = 'Все пары собраны! 🎉';
  1217. }
  1218. } else {
  1219. wrongSound();
  1220. d.classList.add('wrong');
  1221. setTimeout(function(){ d.classList.remove('wrong'); }, 400);
  1222. }
  1223. });
  1224. rightCol.appendChild(d);
  1225. });
  1226. }
  1227. // карточки — переворот
  1228. var cards = document.querySelectorAll('#cards .card');
  1229. for(var ci=0; ci<cards.length; ci++){
  1230. cards[ci].addEventListener('click', function(){
  1231. this.classList.toggle('flipped');
  1232. clickSound();
  1233. });
  1234. }
  1235. document.getElementById('btnNext6').addEventListener('click', function(){ goToScene(6); });
  1236. /* ================= SCENE 7: ТЕСТ ================= */
  1237. var quizQuestions = [
  1238. { q: 'Что переносит механическая волна?', a: ['Вещество', 'Энергию', 'Заряд', 'Массу'], c: 1 },
  1239. { q: 'В какой среде звук распространяется быстрее всего?', a: ['Воздух', 'Вода', 'Сталь', 'Вакуум'], c: 2 },
  1240. { q: 'Формула связи периода и частоты:', a: ['T = \u03BD', 'T = 1/\u03BD', 'T = \u03BD²', 'T = 2\u03BD'], c: 1 },
  1241. { q: 'Звук — это какая волна?', a: ['Поперечная', 'Продольная', 'Электромагнитная', 'Стоячая'], c: 1 },
  1242. { q: 'Скорость звука в воздухе примерно равна:', a: ['34 м/с', '343 м/с', '3430 м/с', '3 м/с'], c: 1 },
  1243. { q: 'Что такое амплитуда?', a: ['Число колебаний в секунду', 'Максимальное отклонение от равновесия', 'Время одного колебания', 'Длина волны'], c: 1 },
  1244. { q: 'Единица измерения частоты:', a: ['Гц', 'м', 'с', 'кг'], c: 0 },
  1245. { q: 'Могут ли механические волны распространяться в вакууме?', a: ['Да', 'Нет', 'Только звук', 'Только свет'], c: 1 },
  1246. { q: 'Связь скорости, длины волны и частоты:', a: ['v = \u03BB/\u03BD', 'v = \u03BB·\u03BD', 'v = \u03BD/\u03BB', 'v = \u03BB+\u03BD'], c: 1 },
  1247. { q: 'Ультразвук — это:', a: ['\u03BD < 20 Гц', '\u03BD > 20 кГц', '\u03BD = 20 Гц', 'Любой звук'], c: 1 }
  1248. ];
  1249. var qIndex = 0, qScore = 0;
  1250. function startQuiz(){
  1251. qIndex = 0; qScore = 0;
  1252. document.getElementById('quizResult').style.display = 'none';
  1253. document.getElementById('quizQuestion').style.display = 'block';
  1254. document.getElementById('quizOptions').style.display = 'flex';
  1255. document.getElementById('quizTitle').textContent = 'Итоговое тестирование';
  1256. showQuestion();
  1257. }
  1258. function showQuestion(){
  1259. var q = quizQuestions[qIndex];
  1260. document.getElementById('quizProgress').style.width = (qIndex / quizQuestions.length * 100) + '%';
  1261. document.getElementById('quizQuestion').textContent = (qIndex+1) + '. ' + q.q;
  1262. var opts = document.getElementById('quizOptions');
  1263. opts.innerHTML = '';
  1264. q.a.forEach(function(text, i){
  1265. var b = document.createElement('button');
  1266. b.className = 'btn';
  1267. b.type = 'button';
  1268. b.style.textTransform = 'none'; // ✅ отключаем КАПС
  1269. b.style.letterSpacing = 'normal'; // ✅ буквы без разрядки
  1270. b.innerHTML = text;
  1271. b.addEventListener('click', function(){
  1272. // блокируем остальные
  1273. var all = opts.querySelectorAll('.btn');
  1274. for(var k=0;k<all.length;k++) all[k].disabled = true;
  1275. if(i === q.c){
  1276. b.classList.add('correct');
  1277. qScore++;
  1278. correctSound();
  1279. } else {
  1280. b.classList.add('wrong');
  1281. all[q.c].classList.add('correct');
  1282. wrongSound();
  1283. }
  1284. setTimeout(function(){
  1285. qIndex++;
  1286. if(qIndex < quizQuestions.length){
  1287. showQuestion();
  1288. } else {
  1289. showResult();
  1290. }
  1291. }, 900);
  1292. });
  1293. opts.appendChild(b);
  1294. });
  1295. }
  1296. function showResult(){
  1297. /* =========================================================
  1298. 🎆 СИСТЕМА ЧАСТИЦ (конфетти + фейерверк)
  1299. ========================================================= */
  1300. var fxCanvas = document.getElementById('fxCanvas');
  1301. var fxCtx = fxCanvas.getContext('2d');
  1302. var particles = [];
  1303. var fxRunning = false;
  1304. var fxMode = null; // 'win' | 'good' | 'meh' | 'lose' | null
  1305. function resizeFx(){
  1306. fxCanvas.width = window.innerWidth;
  1307. fxCanvas.height = window.innerHeight;
  1308. }
  1309. resizeFx();
  1310. window.addEventListener('resize', resizeFx);
  1311. // Одна частица
  1312. function Particle(x, y, opts){
  1313. opts = opts || {};
  1314. this.x = x; this.y = y;
  1315. var angle = opts.angle !== undefined ? opts.angle : Math.random() * Math.PI * 2;
  1316. var speed = opts.speed !== undefined ? opts.speed : 2 + Math.random() * 5;
  1317. this.vx = Math.cos(angle) * speed;
  1318. this.vy = Math.sin(angle) * speed - (opts.lift || 0);
  1319. this.g = opts.g !== undefined ? opts.g : 0.12;
  1320. this.life = opts.life || 80 + Math.random() * 40;
  1321. this.maxLife = this.life;
  1322. this.size = opts.size || 3 + Math.random() * 4;
  1323. this.color = opts.color || ('hsl(' + (Math.random()*360) + ',100%,60%)');
  1324. this.shape = opts.shape || 'rect';
  1325. this.rot = Math.random() * Math.PI;
  1326. this.vr = (Math.random() - 0.5) * 0.3;
  1327. }
  1328. Particle.prototype.update = function(){
  1329. this.x += this.vx;
  1330. this.y += this.vy;
  1331. this.vy += this.g;
  1332. this.vx *= 0.995;
  1333. this.rot += this.vr;
  1334. this.life--;
  1335. };
  1336. Particle.prototype.draw = function(ctx){
  1337. var a = this.life / this.maxLife;
  1338. ctx.save();
  1339. ctx.globalAlpha = a;
  1340. ctx.translate(this.x, this.y);
  1341. ctx.rotate(this.rot);
  1342. ctx.fillStyle = this.color;
  1343. ctx.shadowBlur = 12;
  1344. ctx.shadowColor = this.color;
  1345. if(this.shape === 'circle'){
  1346. ctx.beginPath();
  1347. ctx.arc(0, 0, this.size, 0, Math.PI * 2);
  1348. ctx.fill();
  1349. } else {
  1350. ctx.fillRect(-this.size/2, -this.size/2, this.size, this.size * 1.6);
  1351. }
  1352. ctx.restore();
  1353. };
  1354. // 🌟 Взрыв фейерверка в точке (x, y)
  1355. function fireworkBurst(x, y, hue){
  1356. var count = 50 + Math.floor(Math.random() * 30);
  1357. for(var i=0;i<count;i++){
  1358. var ang = (i / count) * Math.PI * 2 + Math.random() * 0.3;
  1359. var sp = 2 + Math.random() * 6;
  1360. particles.push(new Particle(x, y, {
  1361. angle: ang, speed: sp, g: 0.15, life: 70 + Math.random() * 50,
  1362. size: 3 + Math.random() * 3,
  1363. color: 'hsl(' + (hue + Math.random() * 40) + ',100%,' + (55 + Math.random()*20) + '%)',
  1364. shape: Math.random() > 0.5 ? 'circle' : 'rect'
  1365. }));
  1366. }
  1367. }
  1368. // 🎊 Конфетти, падающее сверху
  1369. function confettiRain(count){
  1370. for(var i=0;i<count;i++){
  1371. particles.push(new Particle(
  1372. Math.random() * fxCanvas.width,
  1373. -10 - Math.random() * 100,
  1374. {
  1375. angle: Math.PI / 2,
  1376. speed: 1 + Math.random() * 3,
  1377. g: 0.05,
  1378. life: 200 + Math.random() * 100,
  1379. size: 3 + Math.random() * 5,
  1380. color: 'hsl(' + (Math.random()*360) + ',100%,65%)',
  1381. shape: 'rect'
  1382. }
  1383. ));
  1384. }
  1385. }
  1386. // 💔 Грустные «капли» — вниз и медленно гаснут
  1387. function sadRain(count){
  1388. for(var i=0;i<count;i++){
  1389. particles.push(new Particle(
  1390. Math.random() * fxCanvas.width,
  1391. -10,
  1392. {
  1393. angle: Math.PI / 2,
  1394. speed: 0.5 + Math.random() * 1.5,
  1395. g: 0.03,
  1396. life: 250,
  1397. size: 3 + Math.random() * 4,
  1398. color: 'hsl(' + (350 + Math.random()*20) + ',80%,55%)',
  1399. shape: 'circle'
  1400. }
  1401. ));
  1402. }
  1403. }
  1404. // 🎬 Основной цикл отрисовки эффектов
  1405. function fxLoop(){
  1406. fxCtx.clearRect(0, 0, fxCanvas.width, fxCanvas.height);
  1407. for(var i = particles.length - 1; i >= 0; i--){
  1408. var p = particles[i];
  1409. p.update();
  1410. p.draw(fxCtx);
  1411. if(p.life <= 0 || p.y > fxCanvas.height + 50){
  1412. particles.splice(i, 1);
  1413. }
  1414. }
  1415. if(particles.length > 0 || fxRunning){
  1416. requestAnimationFrame(fxLoop);
  1417. } else {
  1418. fxRunning = false;
  1419. fxCtx.clearRect(0, 0, fxCanvas.width, fxCanvas.height);
  1420. }
  1421. }
  1422. function startFxLoop(){
  1423. if(fxRunning) return;
  1424. fxRunning = true;
  1425. fxLoop();
  1426. }
  1427. // 🚀 Запуск салюта (серия взрывов в течение 2 секунд)
  1428. function startFireworkShow(duration){
  1429. startFxLoop();
  1430. var startTime = performance.now();
  1431. function scheduleBurst(){
  1432. if(performance.now() - startTime > duration) return;
  1433. var x = fxCanvas.width * (0.2 + Math.random() * 0.6);
  1434. var y = fxCanvas.height * (0.2 + Math.random() * 0.4);
  1435. var hue = Math.random() * 360;
  1436. fireworkBurst(x, y, hue);
  1437. beep(180 + Math.random() * 120, 0.08, 'sawtooth', 0.06); // «бум»
  1438. setTimeout(scheduleBurst, 180 + Math.random() * 280);
  1439. }
  1440. scheduleBurst();
  1441. }
  1442. // =========================================================
  1443. // 🔊 ЗВУКИ ПОБЕДЫ / ПРОИГРЫША
  1444. // =========================================================
  1445. function playVictorySound(){
  1446. if(!soundOn) return;
  1447. ensureAudio();
  1448. if(!audioCtx) return;
  1449. // мажорный аккорд-фанфары: C-E-G-C
  1450. var notes = [523.25, 659.25, 783.99, 1046.50];
  1451. notes.forEach(function(f, i){
  1452. setTimeout(function(){ beep(f, 0.35, 'triangle', 0.18); }, i * 140);
  1453. });
  1454. setTimeout(function(){
  1455. // финальный подъём
  1456. beep(1567.98, 0.6, 'triangle', 0.20);
  1457. }, 700);
  1458. }
  1459. function playGoodSound(){
  1460. if(!soundOn) return;
  1461. beep(523.25, 0.20, 'triangle', 0.18);
  1462. setTimeout(function(){ beep(783.99, 0.30, 'triangle', 0.18); }, 160);
  1463. }
  1464. function playMehSound(){
  1465. if(!soundOn) return;
  1466. beep(440, 0.25, 'sine', 0.15);
  1467. setTimeout(function(){ beep(494, 0.30, 'sine', 0.15); }, 180);
  1468. }
  1469. function playLoseSound(){
  1470. if(!soundOn) return;
  1471. ensureAudio();
  1472. if(!audioCtx) return;
  1473. // нисходящая грустная гамма
  1474. var notes = [440, 392, 349.23, 293.66, 261.63];
  1475. notes.forEach(function(f, i){
  1476. setTimeout(function(){ beep(f, 0.30, 'sawtooth', 0.14); }, i * 180);
  1477. });
  1478. }
  1479. document.getElementById('quizProgress').style.width = '100%';
  1480. document.getElementById('quizQuestion').style.display = 'none';
  1481. document.getElementById('quizOptions').style.display = 'none';
  1482. var res = document.getElementById('quizResult');
  1483. res.style.display = 'flex';
  1484. var pct = Math.round(qScore / quizQuestions.length * 100);
  1485. document.getElementById('finalScore').textContent = qScore + ' / ' + quizQuestions.length;
  1486. var badge = document.getElementById('finalBadge');
  1487. // 🎆 очищаем возможные старые частицы
  1488. particles = [];
  1489. fxCtx.clearRect(0, 0, fxCanvas.width, fxCanvas.height);
  1490. if(pct >= 90){
  1491. // 🏆 ПОБЕДА
  1492. badge.className = 'final-badge win';
  1493. badge.textContent = '🏆 ПОБЕДА!';
  1494. document.getElementById('finalText').textContent = pct + '% — Отлично! Ты отлично разбираешься в волнах!';
  1495. playVictorySound();
  1496. // залп салюта + конфетти
  1497. startFireworkShow(2200);
  1498. confettiRain(120);
  1499. setTimeout(function(){ confettiRain(80); }, 500);
  1500. }
  1501. else if(pct >= 70){
  1502. // 👍 ХОРОШО
  1503. badge.className = 'final-badge good';
  1504. badge.textContent = '👍 ХОРОШО!';
  1505. document.getElementById('finalText').textContent = pct + '% — Хороший результат! Ещё чуть-чуть до идеала.';
  1506. playGoodSound();
  1507. confettiRain(70);
  1508. startFxLoop();
  1509. }
  1510. else if(pct >= 50){
  1511. // 😕 СРЕДНЕ
  1512. badge.className = 'final-badge meh';
  1513. badge.textContent = '😕 МОЖНО ЛУЧШЕ';
  1514. document.getElementById('finalText').textContent = pct + '% — Неплохо, но стоит повторить материал.';
  1515. playMehSound();
  1516. confettiRain(25);
  1517. startFxLoop();
  1518. }
  1519. else {
  1520. // 💔 ПРОВАЛ
  1521. badge.className = 'final-badge lose';
  1522. badge.textContent = '💔 ПОПРОБУЙ ЕЩЁ';
  1523. document.getElementById('finalText').textContent = pct + '% — Материал стоит пройти заново, ты справишься!';
  1524. playLoseSound();
  1525. sadRain(60);
  1526. startFxLoop();
  1527. }
  1528. }
  1529. document.getElementById('btnRestart').addEventListener('click', function(){
  1530. clickSound();
  1531. goToScene(0);
  1532. });
  1533. </script>