project0_67.html 64 KB

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