index.html 163 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608
  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, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
  6. <meta name="theme-color" content="#9C27B0">
  7. <meta name="apple-mobile-web-app-capable" content="yes">
  8. <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
  9. <title>HarmoniX | Academy & InfoSec</title>
  10. <style>
  11. /* ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ И ДИНАМИЧЕСКИЕ ТЕМЫ */
  12. :root {
  13. --bg-color: #f6f8fb; --panel-rgb: 255, 255, 255; --text-color: #1a1a24;
  14. --border-color: rgba(156, 39, 176, 0.35); --accent: #9C27B0;
  15. --base-wave: #2196F3; --accent-alt: #FF5722; --btn-glow: rgba(156, 39, 176, 0.45);
  16. --canvas-bg: rgba(250, 250, 250, 0.9); --glass-alpha: 0.88;
  17. --glass-blur: 16px; --glass-saturate: 135%; --glass-border: rgba(255, 255, 255, 0.85);
  18. --element-border: rgba(156, 39, 176, 0.45); --content-bg: rgba(255, 255, 255, 0.92);
  19. --badge-bg: rgba(156, 39, 176, 0.12); --tzi-color: #00E676; --tzi-glow: rgba(0, 230, 118, 0.5);
  20. --wave-btn-radius: 18px 6px 18px 6px; --wave-btn-hover: 6px 18px 6px 18px;
  21. }
  22. [data-theme="dark"] { --bg-color: #121214; --panel-rgb: 28, 28, 34; --text-color: #eeeeee; --border-color: rgba(187, 134, 252, 0.4); --accent: #bb86fc; --base-wave: #03dac6; --accent-alt: #FF9800; --btn-glow: rgba(187, 134, 252, 0.6); --canvas-bg: rgba(18, 18, 22, 0.9); --glass-border: rgba(255, 255, 255, 0.16); --element-border: rgba(187, 134, 252, 0.5); --content-bg: rgba(28, 28, 34, 0.92); --badge-bg: rgba(187, 134, 252, 0.18); }
  23. [data-theme="neon"] { --bg-color: #000016; --panel-rgb: 10, 10, 30; --text-color: #00ffff; --border-color: rgba(255, 0, 255, 0.7); --accent: #ff00ff; --base-wave: #00ffff; --accent-alt: #FFEA00; --btn-glow: rgba(255, 0, 255, 0.8); --canvas-bg: rgba(0, 0, 0, 0.9); --glass-border: rgba(0, 255, 255, 0.5); --element-border: rgba(255, 0, 255, 0.7); --content-bg: rgba(10, 10, 25, 0.95); --badge-bg: rgba(255, 0, 255, 0.2); }
  24. [data-theme="ocean"] { --bg-color: #0f2027; --panel-rgb: 15, 32, 39; --text-color: #e0f7fa; --border-color: rgba(33, 147, 176, 0.6); --accent: #2193b0; --base-wave: #4facfe; --accent-alt: #FFCA28; --btn-glow: rgba(33, 147, 176, 0.6); --canvas-bg: rgba(10, 20, 25, 0.9); --glass-border: rgba(79, 172, 254, 0.4); --element-border: rgba(33, 147, 176, 0.6); --content-bg: rgba(20, 40, 50, 0.9); --badge-bg: rgba(33, 147, 176, 0.2); }
  25. [data-theme="forest"] { --bg-color: #134e5e; --panel-rgb: 19, 78, 94; --text-color: #e8f5e9; --border-color: rgba(113, 178, 128, 0.6); --accent: #71b280; --base-wave: #a8e063; --accent-alt: #FFB300; --btn-glow: rgba(113, 178, 128, 0.6); --canvas-bg: rgba(10, 40, 30, 0.9); --glass-border: rgba(168, 224, 99, 0.4); --element-border: rgba(113, 178, 128, 0.6); --content-bg: rgba(25, 60, 50, 0.9); --badge-bg: rgba(113, 178, 128, 0.2); }
  26. [data-theme="sunset"] { --bg-color: #2b1055; --panel-rgb: 43, 16, 85; --text-color: #ffe53b; --border-color: rgba(255, 37, 37, 0.6); --accent: #ff2525; --base-wave: #ff2525; --accent-alt: #ff9100; --btn-glow: rgba(255, 37, 37, 0.6); --canvas-bg: rgba(20, 10, 40, 0.9); --glass-border: rgba(255, 229, 59, 0.4); --element-border: rgba(255, 37, 37, 0.6); --content-bg: rgba(40, 20, 60, 0.9); --badge-bg: rgba(255, 37, 37, 0.2); }
  27. [data-theme="cyber"] { --bg-color: #fcee0a; --panel-rgb: 20, 20, 20; --text-color: #00ffff; --border-color: rgba(255, 0, 85, 0.9); --accent: #ff0055; --base-wave: #0ff; --accent-alt: #00E5FF; --btn-glow: rgba(255, 0, 85, 0.8); --canvas-bg: rgba(0, 0, 0, 0.95); --glass-border: rgba(255, 238, 10, 0.7); --element-border: rgba(255, 0, 85, 0.9); --content-bg: rgba(20, 20, 20, 0.95); --badge-bg: rgba(255, 0, 85, 0.25); }
  28. [data-theme="coffee"] { --bg-color: #f3e5f5; --panel-rgb: 239, 222, 205; --text-color: #4e342e; --border-color: rgba(141, 110, 99, 0.6); --accent: #795548; --base-wave: #8d6e63; --accent-alt: #d84315; --btn-glow: rgba(121, 85, 72, 0.6); --canvas-bg: rgba(255, 248, 225, 0.9); --glass-border: rgba(255, 255, 255, 0.7); --element-border: rgba(141, 110, 99, 0.6); --content-bg: rgba(245, 230, 215, 0.9); --badge-bg: rgba(121, 85, 72, 0.2); }
  29. [data-theme="space"] { --bg-color: #000000; --panel-rgb: 15, 10, 30; --text-color: #e0e0e0; --border-color: rgba(126, 87, 194, 0.6); --accent: #7e57c2; --base-wave: #b39ddb; --accent-alt: #00e676; --btn-glow: rgba(126, 87, 194, 0.6); --canvas-bg: rgba(5, 5, 15, 0.9); --glass-border: rgba(179, 157, 219, 0.35); --element-border: rgba(126, 87, 194, 0.6); --content-bg: rgba(20, 15, 40, 0.9); --badge-bg: rgba(126, 87, 194, 0.22); }
  30. [data-theme="sakura"] { --bg-color: #fff0f5; --panel-rgb: 255, 240, 245; --text-color: #880e4f; --border-color: rgba(216, 27, 96, 0.5); --accent: #d81b60; --base-wave: #f06292; --accent-alt: #ff6f00; --btn-glow: rgba(216, 27, 96, 0.6); --canvas-bg: rgba(255, 250, 250, 0.9); --glass-border: rgba(255, 255, 255, 0.8); --element-border: rgba(216, 27, 96, 0.55); --content-bg: rgba(255, 235, 240, 0.9); --badge-bg: rgba(216, 27, 96, 0.18); }
  31. * { box-sizing: border-box; margin: 0; padding: 0; }
  32. html, body { width: 100%; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; color: var(--text-color); background-color: var(--bg-color); overflow-x: hidden; scroll-behavior: smooth; -webkit-tap-highlight-color: transparent; }
  33. .emoji-fix { font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', sans-serif; }
  34. /* ВОЛНОВЫЕ КНОПКИ С КОНТРАСТОМ ДЛЯ ПАНЕЛЕЙ */
  35. .btn, .btn-enter, .premium-link-btn, .lang-btn, .step-btn,
  36. .accordion-btn, .formula-card, .menu-btn-special,
  37. .btn-test-special, .btn-reload-styled, .file-upload-btn,
  38. .glass-controls button, .quiz-option, .tab, .btn-submit-main,
  39. .btn-tzi, .quiz-nav-btn {
  40. border-radius: var(--wave-btn-radius) !important;
  41. transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
  42. border: 2px solid rgba(255,255,255,0.7) !important;
  43. text-shadow: 0 1px 2px rgba(0,0,0,0.4);
  44. }
  45. .btn:hover, .btn-enter:hover, .premium-link-btn:hover, .lang-btn:hover,
  46. .step-btn:hover, .accordion-btn:hover, .formula-card:hover,
  47. .menu-btn-special:hover, .btn-test-special:hover, .btn-reload-styled:hover,
  48. .file-upload-btn:hover, .glass-controls button:hover, .quiz-option:hover,
  49. .tab:hover, .btn-submit-main:hover, .btn-tzi:hover, .quiz-nav-btn:hover {
  50. border-radius: var(--wave-btn-hover) !important;
  51. transform: translateY(-2px);
  52. }
  53. .btn-pause, .btn-medium, .premium-link-btn[style*="FF9800"] {
  54. background: linear-gradient(135deg, #FF6F00, #E64A19) !important;
  55. color: #ffffff !important; border: 2px solid rgba(255, 255, 255, 0.85) !important;
  56. box-shadow: 0 4px 14px rgba(230, 74, 25, 0.45) !important; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5) !important;
  57. }
  58. .btn-pause.active {
  59. background: linear-gradient(135deg, #D81B60, #AD1457) !important;
  60. border-color: #ffffff !important; box-shadow: 0 0 18px rgba(216, 27, 96, 0.7) !important;
  61. }
  62. /* ИСПРАВЛЕННАЯ СУПЕР-ЯРКАЯ КНОПКА FOXFORD */
  63. .premium-link-btn.foxford-btn {
  64. background: linear-gradient(135deg, #FF6D00, #DD2C00) !important;
  65. color: #ffffff !important; border: 2px solid #ffffff !important;
  66. box-shadow: 0 4px 15px rgba(221, 44, 0, 0.5) !important; text-shadow: 0 1px 3px rgba(0,0,0,0.6) !important;
  67. }
  68. #bg-layer { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: radial-gradient(circle at 10% 20%, rgba(156, 39, 176, 0.08), transparent 40%), radial-gradient(circle at 90% 80%, rgba(33, 150, 243, 0.08), transparent 40%), linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px); background-size: 100% 100%, 100% 100%, 30px 30px, 30px 30px; background-color: var(--bg-color); z-index: -2; transition: filter 0.4s ease, background-image 0.4s ease; }
  69. .gradient-text { animation: colorFlow 6s ease infinite; font-weight: bold; }
  70. @keyframes colorFlow { 0% { color: #ff00cc; } 50% { color: #3333ff; } 100% { color: #ff00cc; } }
  71. .glass-panel { background: rgba(var(--panel-rgb), var(--glass-alpha)); backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); border: 1.5px solid var(--glass-border); box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); }
  72. .glass-element { position: relative; overflow: hidden; background: rgba(var(--panel-rgb), calc(var(--glass-alpha) - 0.15)); border: 1.5px solid var(--element-border); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); transition: transform 0.2s cubic-bezier(0.25, 0.8, 0.25, 1), background 0.2s ease, box-shadow 0.2s ease, border-radius 0.3s ease; color: var(--text-color); cursor: pointer; user-select: none; }
  73. .btn-shine::before { content: ''; position: absolute; top: 0; left: -100%; width: 50%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.35), transparent); transform: skewX(-20deg); pointer-events: none; animation: shine 4.5s infinite; }
  74. @keyframes shine { 0% { left: -100%; } 20% { left: 200%; } 100% { left: 200%; } }
  75. #global-tooltip { position: fixed; top: 0; left: 0; z-index: 999999; background: linear-gradient(135deg, rgba(20, 20, 30, 0.98), rgba(40, 20, 40, 0.98)); color: #fff; padding: 14px 18px; border-radius: var(--wave-btn-radius); font-size: 13.5px; max-width: 290px; pointer-events: none; opacity: 0; transition: opacity 0.2s ease, transform 0.15s ease-out; border: 2px solid var(--accent); box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6); line-height: 1.5; text-align: center; font-weight: 500; transform: scale(0.95); }
  76. .hint-icon { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%; background: #FFC107; color: #000; font-size: 12px; font-weight: bold; cursor: help; margin-left: 6px; vertical-align: middle; box-shadow: 0 0 10px rgba(255, 193, 7, 0.6); transition: 0.2s; border: 1.5px solid #fff; }
  77. .hint-icon:hover { transform: scale(1.2); filter: brightness(1.1); box-shadow: 0 0 15px rgba(255, 193, 7, 0.9); }
  78. .pulse-alert { animation: pulseAlert 1.5s infinite; }
  79. @keyframes pulseAlert { 0% { transform: scale(1); filter: brightness(1); } 50% { transform: scale(1.15); filter: brightness(1.2); } 100% { transform: scale(1); filter: brightness(1); } }
  80. @media (hover: hover) and (pointer: fine) {
  81. .glass-element:hover { background: rgba(var(--panel-rgb), calc(var(--glass-alpha) - 0.05)); border-color: var(--accent); box-shadow: 0 6px 16px var(--btn-glow); }
  82. .accordion-btn:hover { background: var(--accent); color: white; padding-left: 22px; }
  83. .info-card:hover { transform: translateY(-4px); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); border-color: var(--accent); }
  84. .btn-enter:hover { transform: translateY(-4px) scale(1.04) !important; box-shadow: 0 15px 40px var(--btn-glow) !important; }
  85. .interactive-hover:hover { box-shadow: 0 8px 25px var(--btn-glow); }
  86. }
  87. .glass-element:active { transform: scale(0.96) !important; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2) !important; }
  88. .btn-enter { position: relative; z-index: 10; padding: 16px 44px; font-size: 22px; font-weight: bold; color: #fff !important; background: linear-gradient(135deg, var(--accent), #ff00cc) !important; border: 2px solid rgba(255, 255, 255, 0.9) !important; border-radius: 35px; box-shadow: 0 0 25px var(--btn-glow); cursor: pointer; transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); outline: none; }
  89. .btn-enter:active { transform: scale(0.96) !important; }
  90. /* ИСПРАВЛЕННАЯ ВЕРХНЯЯ ПАНЕЛЬ С КРУГЛЫМ ЛОГОТИПОМ, КНОПКОЙ ФУЛЛСКРИН И ОБНОВИТЬ */
  91. .top-bar-wrapper { display: none; position: fixed; top: 0; left: 0; width: 100%; z-index: 2000; padding: max(10px, env(safe-area-inset-top)) 10px 10px; pointer-events: none; justify-content: center; }
  92. .top-bar { pointer-events: auto; display: inline-flex; align-items: center; gap: clamp(6px, 1.5vw, 12px); padding: 8px 16px; border-radius: 35px; box-shadow: 0 10px 35px rgba(0, 0, 0, 0.3); background: rgba(var(--panel-rgb), 0.98); border: 2px solid var(--accent); }
  93. .nav-logo { font-size: clamp(16px, 3.5vw, 20px); display: flex; align-items: center; gap: 8px; padding: 4px 8px; font-weight: 900; background: transparent !important; border: none !important; cursor: pointer; }
  94. .nav-logo .logo-super-shape.small { width: 36px; height: 36px; animation: morphRing 3s ease-in-out infinite alternate, ringGlow 2s infinite alternate; }
  95. .menu-btn-special { font-size: clamp(13px, 2vw, 15px); font-weight: 900; background: linear-gradient(135deg, #7B1FA2, #E91E63) !important; color: white !important; cursor: pointer; padding: 10px 22px; display: flex; align-items: center; gap: 8px; box-shadow: 0 4px 15px rgba(123, 31, 162, 0.45) !important; }
  96. .btn-test-special { font-size: clamp(13px, 2vw, 15px); font-weight: 900; background: linear-gradient(135deg, #FF5722, #C2185B) !important; color: white !important; cursor: pointer; padding: 10px 18px; display: flex; align-items: center; gap: 8px; box-shadow: 0 4px 15px rgba(233, 30, 99, 0.45) !important; }
  97. .btn-reload-styled { font-size: clamp(13px, 2vw, 15px); font-weight: 900; background: linear-gradient(135deg, #0091EA, #0052cc) !important; color: white !important; cursor: pointer; padding: 10px 16px; display: flex; align-items: center; gap: 8px; box-shadow: 0 4px 15px rgba(0, 145, 234, 0.45) !important; }
  98. .btn-fullscreen-compact { width: 40px; height: 40px; padding: 0; display: flex; align-items: center; justify-content: center; font-size: 18px; flex-shrink: 0; background: rgba(var(--panel-rgb), 0.9); border: 2px solid var(--border-color) !important; color: var(--text-color); box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
  99. .lang-btn { flex: 1; padding: 10px; font-weight: bold; cursor: pointer; color: var(--text-color); display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 14px; background: rgba(var(--panel-rgb), 0.85); border: 1.5px solid var(--border-color) !important; }
  100. .lang-btn.active { border-color: var(--accent) !important; background: var(--accent) !important; color: white; box-shadow: 0 0 16px var(--btn-glow); text-shadow: 0 1px 3px rgba(0,0,0,0.4); }
  101. .logo-super-shape { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 120px; height: 120px; padding: 6px; background: linear-gradient(135deg, var(--accent), #ff00cc, #00d4ff, var(--accent-alt)); background-size: 300% 300%; border-radius: 32% 68% 65% 35% / 40% 35% 65% 60%; animation: morphRing 5s ease-in-out infinite alternate, ringGlow 3s infinite alternate; margin-bottom: 10px; }
  102. .logo-super-shape.small { width: 32px; height: 32px; padding: 2px; border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; margin-bottom: 0; }
  103. .logo-inner-content { background: var(--bg-color); border-radius: inherit; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; box-shadow: inset 0 0 12px rgba(0, 0, 0, 0.3); }
  104. @keyframes morphRing { 0% { border-radius: 32% 68% 65% 35% / 40% 35% 65% 60%; } 50% { border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; } 100% { border-radius: 40% 60% 70% 30% / 40% 60% 40% 60%; } }
  105. @keyframes ringGlow { 0% { box-shadow: 0 0 15px var(--btn-glow); } 100% { box-shadow: 0 0 35px var(--accent); } }
  106. #welcome-screen { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; display: flex; flex-direction: column; justify-content: center; align-items: center; background: radial-gradient(circle at center, rgba(156, 39, 176, 0.1) 0%, var(--bg-color) 100%); z-index: 9000; transition: opacity 0.35s ease; padding: 20px; text-align: center; }
  107. .welcome-orb { position: absolute; border-radius: 50%; filter: blur(80px); z-index: -1; opacity: 0.6; animation: floatOrb 10s infinite alternate ease-in-out; pointer-events: none; }
  108. .orb-1 { width: 50vmin; height: 50vmin; background: var(--accent); top: -5%; left: -5%; }
  109. .orb-2 { width: 40vmin; height: 40vmin; background: var(--base-wave); bottom: -5%; right: -5%; animation-delay: -3s; }
  110. @keyframes floatOrb { 0% { transform: translate(0, 0) scale(1); } 100% { transform: translate(50px, 50px) scale(1.1); } }
  111. .app-title { font-size: clamp(34px, 8vw, 64px); margin: 20px 0 5px; line-height: 1.2; letter-spacing: 1px; }
  112. .app-subtitle { font-size: clamp(14px, 3.5vw, 18px); color: var(--text-color); margin-bottom: 35px; opacity: 0.85; max-width: 600px; font-weight: 500; }
  113. #main-app { display: none; width: 100%; max-width: 1300px; margin: 0 auto; padding: calc(max(10px, env(safe-area-inset-top)) + 80px) 14px 40px; flex-direction: column; }
  114. #sidebar { position: fixed; left: -360px; top: 0; width: 320px; max-width: 85%; height: 100vh; z-index: 3000; transition: left 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); padding: max(20px, env(safe-area-inset-top)) 20px 30px; overflow-y: auto; display: flex; flex-direction: column; border-right: 2px solid var(--accent); }
  115. #sidebar.open { left: 0; }
  116. #sidebar-overlay { display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.5); z-index: 1999; backdrop-filter: blur(4px); }
  117. #sidebar-overlay.show { display: block; }
  118. .sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 1px solid var(--border-color); padding-bottom: 10px; }
  119. .close-sidebar { font-size: 34px; cursor: pointer; color: var(--text-color); transition: 0.2s; padding: 5px; }
  120. .sidebar-section { margin-bottom: 20px; }
  121. .sidebar-title { font-size: 13px; text-transform: uppercase; color: var(--accent); margin-bottom: 10px; font-weight: bold; letter-spacing: 0.5px; }
  122. .sidebar-btn-asym { padding: 12px 16px; margin-bottom: 8px; font-weight: bold; display: flex; align-items: center; gap: 10px; font-size: 14px; border-left: 4px solid var(--accent); cursor: pointer; }
  123. .sidebar-btn-asym:hover { transform: translateX(8px); background: var(--accent); color: white; }
  124. .setting-block { margin-bottom: 15px; }
  125. .setting-block label { display: block; margin-bottom: 6px; font-weight: bold; font-size: 14px; }
  126. .setting-block select { width: 100%; padding: 10px; border-radius: 8px; font-size: 14px; font-family: inherit; outline: none; border: 1.5px solid var(--element-border); background: rgba(var(--panel-rgb), 0.95); color: var(--text-color); font-weight: bold; }
  127. /* ВЕРНУЛ ОБОИ И КНОПКИ СБРОСА */
  128. .file-upload-wrapper { position: relative; overflow: hidden; display: block; margin-bottom: 8px; }
  129. .file-upload-input { position: absolute; left: 0; top: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; z-index: 2; }
  130. .file-upload-btn { background: rgba(var(--panel-rgb), 0.9); border: 2px dashed var(--accent) !important; color: var(--text-color); padding: 11px; text-align: center; font-weight: 800; display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 14px; }
  131. .glass-controls { display: flex; gap: 6px; margin-bottom: 10px; }
  132. .glass-controls button { flex: 1; padding: 9px; font-size: 13px; font-weight: 800; cursor: pointer; border: 1.5px solid var(--element-border) !important; background: rgba(var(--panel-rgb), 0.9); color: var(--text-color); }
  133. .btn-glass-on.active { background: var(--accent) !important; color: white !important; border-color: #fff !important; box-shadow: 0 0 12px var(--btn-glow) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.5); }
  134. .btn-tzi { background: var(--tzi-color) !important; color: #000 !important; border: 2px solid #fff !important; box-shadow: 0 0 16px var(--tzi-glow); font-weight: 900; }
  135. input[type=range] { width: 100%; margin-top: 5px; accent-color: var(--accent); cursor: pointer; }
  136. .grid-container { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 16px; min-height: 75vh; margin-bottom: 30px; }
  137. .panel { padding: 16px; background: rgba(var(--panel-rgb), 0.85); border: 1px solid var(--border-color); display: flex; flex-direction: column; min-height: 0; position: relative; border-radius: 16px; }
  138. .panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; flex-shrink: 0; flex-wrap: wrap; gap: 8px; }
  139. .canvas-wrapper { flex-grow: 1; position: relative; width: 100%; height: 260px; min-height: 260px; border-radius: 12px; overflow: hidden; border: 2px solid var(--element-border); background: var(--canvas-bg); }
  140. canvas { display: block; position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
  141. .btn-download-compact { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; font-size: 13px; font-weight: bold; cursor: pointer; border: 1.5px solid var(--accent) !important; color: var(--accent); background: rgba(var(--panel-rgb), 0.9); transition: all 0.2s ease; }
  142. .btn-download-compact:hover { background: var(--accent); color: #fff; box-shadow: 0 4px 12px var(--btn-glow); text-shadow: 0 1px 2px rgba(0,0,0,0.4); }
  143. .chart-telemetry { position: absolute; bottom: 8px; left: 8px; z-index: 5; display: flex; flex-wrap: wrap; gap: 6px; pointer-events: none; }
  144. .chart-telemetry.hidden { display: none; }
  145. .telemetry-chip { background: rgba(var(--panel-rgb), 0.96); border: 1.5px solid var(--element-border); padding: 4px 10px; border-radius: 8px; font-size: 12px; font-weight: 700; color: var(--text-color); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); backdrop-filter: blur(8px); font-variant-numeric: tabular-nums; }
  146. .telemetry-chip .val-highlight { color: var(--accent); font-weight: 900; font-size: 13px; }
  147. .telemetry-chip .val-fixed { display: inline-block; min-width: 42px; text-align: right; }
  148. .tabs { display: flex; gap: 8px; margin: 8px 0; flex-shrink: 0; width: 100%; }
  149. .tab { padding: 9px 12px; flex: 1; text-align: center; font-weight: 900; font-size: 15px; cursor: pointer; border: 2px solid var(--element-border) !important; background: rgba(var(--panel-rgb), 0.85); color: var(--text-color); }
  150. .tab.active { background: var(--accent) !important; color: #fff !important; border-color: #fff !important; transform: scale(1.02); box-shadow: 0 4px 14px var(--btn-glow); text-shadow: 0 1px 2px rgba(0,0,0,0.5); }
  151. .presets-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 8px; width: 100%; }
  152. .presets-row button { padding: 9px 4px; font-size: 13px; font-weight: 800; cursor: pointer; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; min-width: 0; text-align: center; border: 1.5px solid var(--element-border) !important; background: rgba(var(--panel-rgb), 0.85); color: var(--text-color); transition: 0.2s; }
  153. .presets-row button.active { background: var(--accent) !important; color: #fff !important; border-color: #fff !important; box-shadow: 0 0 12px var(--btn-glow) !important; transform: scale(1.03); text-shadow: 0 1px 2px rgba(0,0,0,0.5); }
  154. .control-row { margin-bottom: 11px; display: flex; align-items: center; flex-shrink: 0; gap: 10px; width: 100%; }
  155. .control-label { width: 135px; font-weight: bold; font-size: 14px; flex-shrink: 0; display: flex; justify-content: space-between; align-items: center; }
  156. .control-body { display: flex; align-items: center; flex-grow: 1; gap: 10px; width: 100%; }
  157. .secondary-params { background: rgba(var(--panel-rgb), 0.5); border: 2px dashed var(--element-border); border-radius: 14px; padding: 12px; margin-top: 8px; }
  158. .step-toggle { display: flex; background: rgba(var(--panel-rgb), 0.8); border-radius: 12px; padding: 4px; border: 1.5px solid var(--element-border); flex-grow: 1; }
  159. .step-btn { flex: 1; padding: 6px 12px; text-align: center; font-size: 14px; font-weight: 800; cursor: pointer; color: var(--text-color); transition: 0.3s; }
  160. .step-btn.active { background: var(--accent); color: white; box-shadow: 0 2px 8px var(--btn-glow); text-shadow: 0 1px 2px rgba(0,0,0,0.5); }
  161. .input-wrapper { display: flex; align-items: center; gap: 5px; flex-shrink: 0; }
  162. .value-input { width: 80px; padding: 6px; border-radius: 8px; border: 2px solid var(--accent); font-weight: 900; font-size: 15px; text-align: center; outline: none; background: var(--badge-bg); color: var(--accent); font-family: monospace; transition: all 0.25s ease; }
  163. .value-input:focus { box-shadow: 0 0 12px var(--btn-glow); background: rgba(var(--panel-rgb), 0.98); transform: scale(1.05); }
  164. .slider-alt { accent-color: var(--accent-alt) !important; }
  165. .input-alt { border-color: var(--accent-alt) !important; color: var(--accent-alt) !important; background: rgba(230, 74, 25, 0.12) !important; }
  166. .btn-group { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
  167. .btn { padding: 11px 14px; font-weight: 900; color: white; flex-grow: 1; text-align: center; font-family: inherit; font-size: 14px; cursor: pointer; border: 2px solid rgba(255, 255, 255, 0.85) !important; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); }
  168. .btn-audio { background: linear-gradient(135deg, #0288D1, #01579B) !important; box-shadow: 0 4px 12px rgba(2, 136, 209, 0.4) !important; }
  169. .btn-audio.active { background: linear-gradient(135deg, #2E7D32, #1B5E20) !important; border-color: #A5D6A7 !important; box-shadow: 0 0 16px rgba(46, 125, 50, 0.7) !important; }
  170. .btn-dist { background: linear-gradient(135deg, #8E24AA, #4A148C) !important; box-shadow: 0 4px 12px rgba(142, 36, 170, 0.4) !important; }
  171. .btn-dist.active { background: linear-gradient(135deg, #D50000, #B71C1C) !important; border-color: #FF8A80 !important; box-shadow: 0 0 16px rgba(213, 0, 0, 0.7) !important; }
  172. .btn-reset { background: linear-gradient(135deg, #546E7A, #37474F) !important; box-shadow: 0 4px 12px rgba(84, 110, 122, 0.4) !important; }
  173. .site-content { padding: 35px; border-radius: 20px; margin-bottom: 30px; background: var(--content-bg); border: 2px solid var(--border-color); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12); }
  174. .content-header { text-align: center; margin-bottom: 35px; font-size: clamp(22px, 5vw, 28px); font-weight: 800; }
  175. .info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 30px; }
  176. .info-card { padding: 22px; background: rgba(var(--panel-rgb), 0.85); border: 2px solid var(--border-color); border-radius: 16px; }
  177. .info-card h3 { color: var(--accent); margin-top: 0; font-size: 18px; font-weight: 900; }
  178. .info-card p { line-height: 1.6; font-size: 15px; margin-bottom: 0; }
  179. .key-term { font-weight: 800; letter-spacing: 0.5px; color: var(--accent); text-decoration: underline dotted; }
  180. .faq-link { color: var(--base-wave); text-decoration: none; font-weight: bold; transition: color 0.2s; display: inline-block; margin-top: 6px; }
  181. .faq-link:hover { color: var(--accent); text-decoration: underline; }
  182. .calc-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 40px; }
  183. .wave-calculator { background: #111119; border: 2.5px solid #2a2a38; padding: 22px; border-radius: 20px; text-align: center; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35); color: #fff; }
  184. .calc-screen { background: #051007 !important; border: 2px solid #1b5e20; border-radius: 12px; padding: 16px; box-shadow: inset 0 0 15px rgba(0, 230, 118, 0.15); margin-bottom: 15px; }
  185. .calc-formula { font-family: 'Courier New', monospace; font-size: 18px; color: #81c784; margin-bottom: 8px; font-weight: bold; }
  186. .calc-result { font-family: 'Courier New', monospace; font-size: 26px; font-weight: 900; color: #00E676; text-shadow: 0 0 10px rgba(0, 230, 118, 0.7); margin-top: 5px; }
  187. .calc-controls { display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
  188. .calc-input { font-size: 16px; padding: 8px; width: 110px; text-align: center; border-radius: 8px; border: 2px solid #444; background: #0a0a10 !important; color: #00E676 !important; font-weight: 900; outline: none; font-family: 'Courier New', monospace; transition: all 0.3s; }
  189. .calc-input:focus { border-color: #00E676; box-shadow: 0 0 15px rgba(0, 230, 118, 0.5); transform: scale(1.04); }
  190. .calc-select { font-size: 14px; padding: 6px; border-radius: 8px; border: 2px solid #444; background: #0a0a10; color: #00E676; font-weight: bold; outline: none; font-family: 'Courier New', monospace; cursor: pointer; transition: 0.3s; max-width: 150px; }
  191. .calc-select:focus { border-color: #00E676; box-shadow: 0 0 15px rgba(0,230,118,0.5); }
  192. .accordion { width: 100%; margin-top: 15px; }
  193. .accordion-btn { background: rgba(var(--panel-rgb), 0.95); color: var(--text-color); cursor: pointer; padding: 16px 22px; width: 100%; border: 2px solid var(--border-color); text-align: left; outline: none; font-size: 16px; font-weight: bold; display: flex; justify-content: space-between; margin-bottom: 8px; }
  194. .accordion-btn.active { background: var(--accent); color: #fff; border-bottom-left-radius: 0; border-bottom-right-radius: 0; margin-bottom: 0; }
  195. .accordion-content { padding: 0 22px; background: rgba(var(--panel-rgb), 0.85); max-height: 0; overflow: hidden; transition: max-height 0.3s ease-out; border-left: 2px solid var(--accent); border-right: 2px solid var(--accent); border-bottom: 2px solid var(--accent); border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; margin-bottom: 8px; }
  196. .accordion-content p { padding: 16px 0; line-height: 1.6; margin: 0; font-size: 15px; }
  197. .resource-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; margin-top: 20px; }
  198. .premium-link-btn { display: flex; align-items: center; justify-content: center; gap: 10px; padding: 14px 18px; font-weight: 900; font-size: 14px; color: #fff !important; text-decoration: none; font-family: inherit; border: 2px solid rgba(255, 255, 255, 0.7) !important; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18); }
  199. .premium-link-btn.sim-btn { background: linear-gradient(135deg, #00C853, #00796B); }
  200. .premium-link-btn.wiki-btn { background: linear-gradient(135deg, #1976D2, #0D47A1); }
  201. .premium-link-btn.youtube-btn { background: linear-gradient(135deg, #E53935, #B71C1C); }
  202. .premium-link-btn.ru-btn { background: linear-gradient(135deg, #303F9F, #1A237E); }
  203. .premium-link-btn.rutube-btn { background: linear-gradient(135deg, #111, #222); border-color: #00D1FF !important; }
  204. .app-footer { text-align: center; font-size: 13px; opacity: 0.85; padding: 25px 15px 40px; border-top: 1.5px solid var(--border-color); font-weight: bold; margin-top: 20px; }
  205. .modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.75); z-index: 3500; justify-content: center; align-items: center; padding: 12px; backdrop-filter: blur(10px); opacity: 0; transition: opacity 0.3s ease; }
  206. .modal-overlay.show { opacity: 1; display: flex; }
  207. .modal-content { padding: 28px; border-radius: 22px; width: 100%; max-width: 800px; max-height: 85vh; overflow-y: auto; position: relative; border: 2.5px solid var(--accent); }
  208. .close-btn { position: absolute; top: 12px; right: 15px; font-size: 30px; cursor: pointer; color: var(--text-color); padding: 4px; z-index: 10; transition: transform 0.2s; font-weight: bold; }
  209. .close-btn:hover { transform: scale(1.15); color: var(--accent); }
  210. .about-section { background: rgba(var(--panel-rgb), 0.8); border: 1.5px solid var(--border-color); padding: 20px; margin-bottom: 18px; border-radius: 16px; }
  211. .about-section h3 { margin-top: 0; color: var(--accent); display: flex; align-items: center; gap: 10px; font-size: 18px; }
  212. .about-section p { line-height: 1.6; font-size: 15px; margin-bottom: 0; }
  213. .timeline { border-left: 3px solid var(--accent); padding-left: 18px; margin-left: 10px; position: relative; margin-top: 15px; }
  214. .timeline-item { position: relative; margin-bottom: 18px; font-size: 14px; line-height: 1.6; background: rgba(var(--panel-rgb), 0.6); padding: 12px; border-radius: 8px; border: 1px solid var(--border-color); transition: 0.2s; }
  215. .timeline-item:hover { background: rgba(var(--panel-rgb), 0.9); transform: translateX(5px); }
  216. .timeline-item::before { content: ''; position: absolute; left: -26px; top: 14px; width: 12px; height: 12px; border-radius: 50%; background: var(--accent); border: 2px solid var(--bg-color); box-shadow: 0 0 8px var(--btn-glow); }
  217. .timeline-item b { color: var(--accent); display: block; font-size: 15px; margin-bottom: 4px; }
  218. .formula-grid { display: grid; grid-template-columns: 1fr; gap: 12px; margin-bottom: 24px; }
  219. @media (min-width: 600px) { .formula-grid { grid-template-columns: 1fr 1fr; } }
  220. .formula-card { padding: 14px 18px; font-size: 14px; line-height: 1.6; border-left: 4px solid var(--accent); background: rgba(var(--panel-rgb), 0.8); font-family: monospace; font-weight: bold; border-radius: 12px; transition: 0.3s; }
  221. .formula-card:hover { transform: translateY(-3px); box-shadow: 0 4px 15px var(--btn-glow); }
  222. .formula-card b { display: block; color: var(--accent); margin-bottom: 6px; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; font-family: -apple-system, sans-serif; }
  223. /* ИНТЕРАКТИВНЫЕ ТЕСТЫ И СЛАЙДЕР С SVG АНИМАЦИЕЙ */
  224. .quiz-timer-pill { position: sticky; top: -10px; z-index: 100; width: 100%; max-width: 440px; margin: 0 auto 20px auto; background: rgba(15, 15, 25, 0.95); border: 2px solid var(--accent); border-radius: 40px; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4); backdrop-filter: blur(14px); display: flex; flex-direction: column; overflow: hidden; }
  225. .timer-progress-bg { width: 100%; height: 8px; background: rgba(255, 255, 255, 0.12); position: relative; }
  226. .timer-progress-fill { height: 100%; width: 100%; background: linear-gradient(90deg, #00E676 0%, #00B0FF 100%); box-shadow: 0 0 12px rgba(0, 230, 118, 0.6); transition: width 1s linear, background 0.4s ease, box-shadow 0.4s ease; }
  227. .timer-content { display: flex; justify-content: space-between; align-items: center; width: 100%; padding: 10px 20px; position: relative; z-index: 2; }
  228. .timer-display { color: #00E676; font-size: 22px; font-weight: bold; font-family: monospace; letter-spacing: 1px; transition: color 0.3s; text-shadow: 0 0 10px rgba(0, 230, 118, 0.5); }
  229. .timer-orange { color: #FF9800 !important; text-shadow: 0 0 10px rgba(255, 152, 0, 0.6); }
  230. .timer-red-blink { color: #f44336 !important; animation: timerBlink 1s infinite; text-shadow: 0 0 15px rgba(244, 67, 54, 0.8); }
  231. @keyframes timerBlink { 0%, 100% { opacity: 1; text-shadow: 0 0 18px rgba(244, 67, 54, 0.9); } 50% { opacity: 0.5; text-shadow: 0 0 6px rgba(244, 67, 54, 0.4); } }
  232. .test-modes { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; margin-top: 25px; }
  233. .test-modes .btn { font-size: 15px; padding: 16px 10px; color: #ffffff !important; font-weight: 900; text-align: center; border: 2px solid rgba(255, 255, 255, 0.8) !important; cursor: pointer; text-shadow: 0 1px 3px rgba(0,0,0,0.6); display: flex; flex-direction: column; align-items: center; justify-content: center; transition: all 0.3s ease !important; }
  234. .btn-easy { background: linear-gradient(135deg, #4CAF50, #2E7D32) !important; box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3) !important; }
  235. .btn-easy:hover { box-shadow: 0 0 20px rgba(76, 175, 80, 0.9) !important; border-color: #fff !important; filter: brightness(1.15); }
  236. .btn-medium { background: linear-gradient(135deg, #FF9800, #EF6C00) !important; box-shadow: 0 4px 15px rgba(255, 152, 0, 0.3) !important; }
  237. .btn-medium:hover { box-shadow: 0 0 20px rgba(255, 152, 0, 0.9) !important; border-color: #fff !important; filter: brightness(1.15); }
  238. .btn-hard { background: linear-gradient(135deg, #F44336, #C62828) !important; box-shadow: 0 4px 15px rgba(244, 67, 54, 0.3) !important; }
  239. .btn-hard:hover { box-shadow: 0 0 20px rgba(244, 67, 54, 0.9) !important; border-color: #fff !important; filter: brightness(1.15); }
  240. .btn-ultra { background: linear-gradient(135deg, #9C27B0, #E91E63) !important; border: 2.5px solid #FF80AB !important; box-shadow: 0 4px 15px rgba(156, 39, 176, 0.3) !important; }
  241. .btn-ultra:hover { box-shadow: 0 0 20px rgba(233, 30, 99, 0.9) !important; border-color: #fff !important; filter: brightness(1.15); }
  242. .question-card-wrapper { margin-top: 10px; min-height: 250px; }
  243. .question { margin-bottom: 20px; padding: 24px; background: rgba(var(--panel-rgb), 0.85); border: 2px solid var(--border-color); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08); border-radius: var(--wave-btn-radius); animation: fadeInUp 0.4s ease-out forwards; }
  244. @keyframes fadeInUp { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
  245. .question-meta { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; font-weight: 900; font-size: 15px; color: var(--accent); }
  246. .question p { font-weight: bold; margin-top: 0; margin-bottom: 20px; font-size: 18px; line-height: 1.5; color: var(--text-color); }
  247. .quiz-option { display: flex; align-items: center; padding: 16px 20px; margin-bottom: 12px; font-size: 16px; cursor: pointer; border: 2px solid var(--element-border); background: rgba(var(--panel-rgb), 0.9); transition: all 0.25s cubic-bezier(0.25, 0.8, 0.25, 1); position: relative; font-weight: bold; }
  248. .quiz-option input { display: none; }
  249. .custom-radio { width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--accent); margin-right: 14px; position: relative; flex-shrink: 0; transition: 0.2s; }
  250. .quiz-option.selected { border-color: var(--accent); background: rgba(156, 39, 176, 0.15); transform: scale(1.02); box-shadow: 0 4px 15px rgba(156, 39, 176, 0.3); border-radius: var(--wave-btn-hover) !important; }
  251. .quiz-option.selected .custom-radio { border-color: var(--accent); box-shadow: 0 0 10px var(--btn-glow); }
  252. .quiz-option.selected .custom-radio::after { content: ''; position: absolute; top: 4px; left: 4px; width: 10px; height: 10px; background: var(--accent); border-radius: 50%; }
  253. .quiz-option.selected .option-text { color: var(--accent); }
  254. .quiz-option.correct-answer { background: rgba(76, 175, 80, 0.22) !important; border-color: #4CAF50 !important; color: #1B5E20 !important; box-shadow: 0 0 14px rgba(76, 175, 80, 0.4); }
  255. .quiz-option.correct-answer::after { content: '✔'; position: absolute; right: 16px; color: #2E7D32; font-size: 20px; font-weight: 900; }
  256. .quiz-option.wrong-answer { background: rgba(244, 67, 54, 0.22) !important; border-color: #f44336 !important; color: #B71C1C !important; box-shadow: 0 0 14px rgba(244, 67, 54, 0.4); }
  257. .quiz-option.wrong-answer::after { content: '✖'; position: absolute; right: 16px; color: #C62828; font-size: 20px; font-weight: 900; }
  258. .quiz-nav-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-top: 15px; }
  259. .quiz-nav-btn { padding: 12px 24px; font-weight: 900; font-size: 15px; cursor: pointer; border: 2px solid var(--accent); background: rgba(var(--panel-rgb), 0.95); color: var(--text-color); }
  260. .quiz-nav-btn:disabled { opacity: 0.35; cursor: not-allowed; transform: none !important; }
  261. #test-result-screen { display: none; text-align: center; padding: 15px 5px; }
  262. /* ИНТЕРАКТИВНЫЙ КРУГ SVG */
  263. .score-circle-wrapper { position: relative; width: 140px; height: 140px; margin: 0 auto 20px auto; }
  264. .circular-progress { transform: rotate(-90deg); width: 140px; height: 140px; filter: drop-shadow(0 0 10px var(--accent)); transition: filter 0.5s; }
  265. .circular-progress circle { fill: none; stroke-width: 10; stroke-linecap: round; }
  266. .circular-progress .bg { stroke: rgba(var(--panel-rgb), 0.5); }
  267. .circular-progress .progress { stroke: var(--accent); stroke-dasharray: 339.292; stroke-dashoffset: 339.292; transition: stroke-dashoffset 1s ease-out, stroke 0.5s; }
  268. .score-text { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 32px; font-weight: 900; color: var(--accent); transition: color 0.5s; }
  269. .review-box { margin-top: 20px; text-align: left; max-height: 300px; overflow-y: auto; background: rgba(var(--panel-rgb), 0.9); border: 2px solid var(--element-border); padding: 15px; border-radius: 16px; animation: fadeInUp 0.6s ease-out forwards; }
  270. .btn-submit-main { background: #00E676 !important; color: #fff !important; font-size: 16px; padding: 10px 25px; box-shadow: 0 5px 20px rgba(0, 230, 118, 0.5) !important; border: 2px solid #fff !important; cursor: pointer; font-weight: 900; transition: 0.2s; }
  271. .btn-submit-main:active { transform: scale(0.95); }
  272. @media screen and (max-width: 820px) {
  273. #main-app { padding: calc(max(10px, env(safe-area-inset-top)) + 70px) 10px 20px 10px; }
  274. .grid-container { display: flex; flex-direction: column; gap: 16px; margin-bottom: 20px; }
  275. .panel { padding: 16px; }
  276. .canvas-wrapper { height: 260px; min-height: 260px; }
  277. .presets-row { grid-template-columns: repeat(4, 1fr); gap: 4px; }
  278. .presets-row button { padding: 10px 2px; font-size: 11px; }
  279. .control-row { flex-direction: column; align-items: flex-start; gap: 8px; margin-bottom: 12px; }
  280. .control-label { width: 100%; }
  281. .control-body { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
  282. .btn-group { flex-direction: column; gap: 10px; margin-top: 10px; }
  283. .btn { width: 100%; padding: 14px; font-size: 15px; }
  284. .info-grid, .calc-grid { grid-template-columns: 1fr; }
  285. .site-content { padding: 18px; }
  286. .modal-content { padding: 20px; }
  287. .test-modes { grid-template-columns: 1fr; }
  288. .hide-mobile { display: none; }
  289. /* Восстановлены кнопки на мобильном для верхней панели (кроме фуллскрина) */
  290. .btn-reload-styled { display: flex; }
  291. }
  292. </style>
  293. </head>
  294. <body style="background-color: var(--bg-color);">
  295. <div id="bg-layer"></div>
  296. <div id="sidebar-overlay" onclick="toggleSidebar()"></div>
  297. <div id="global-tooltip"></div>
  298. <div id="welcome-screen">
  299. <div class="welcome-bg-grid"></div>
  300. <div class="welcome-orb orb-1"></div>
  301. <div class="welcome-orb orb-2"></div>
  302. <div class="logo-super-shape">
  303. <div class="logo-inner-content"><span class="emoji-fix" style="font-size: 44px;">〽️</span></div>
  304. </div>
  305. <h1 class="app-title gradient-text" data-i18n="appTitle" style="margin-top:20px;">HarmoniX</h1>
  306. <h2 class="app-subtitle" data-i18n="appSubtitle">Интерактивная Лаборатория Волновой Физики</h2>
  307. <div style="margin-bottom: 25px; font-size: 13.5px; font-weight: bold; color: var(--accent); background: rgba(var(--panel-rgb), 0.95); padding: 8px 18px; border-radius: var(--wave-btn-radius); border: 2px solid var(--accent); box-shadow: 0 0 15px rgba(156, 39, 176, 0.35);">
  308. <span class="emoji-fix">❗️</span> <span data-i18n="hintYellow">Ищите желтые кружки [?] для подсказок!</span>
  309. </div>
  310. <button class="btn-enter glass-element interactive-hover btn-shine" onclick="startApp()"><span class="emoji-fix">▶</span> <span data-i18n="btnStart">Начать работу</span></button>
  311. </div>
  312. <!-- МЕНЮ НАСТРОЕК (ВОССТАНОВЛЕНО 100%) -->
  313. <div id="sidebar" class="glass-panel">
  314. <div class="sidebar-header">
  315. <h2 class="gradient-text" style="margin:0; font-size: 18px;"><span class="emoji-fix">⚙</span> <span data-i18n="menuSettings">Настройки</span></h2>
  316. <div class="close-sidebar" onclick="toggleSidebar(); playAudioCue('menuClick');">&times;</div>
  317. </div>
  318. <div class="sidebar-section">
  319. <h3 class="sidebar-title"><span class="emoji-fix">🌐</span> Язык / Language</h3>
  320. <div class="glass-controls">
  321. <button class="lang-btn lang-ru active interactive-hover" onclick="setLang('ru'); playAudioCue('menuClick');">🇷🇺 Русский</button>
  322. <button class="lang-btn lang-en interactive-hover" onclick="setLang('en'); playAudioCue('menuClick');">🇬🇧 English</button>
  323. </div>
  324. </div>
  325. <div class="sidebar-section">
  326. <h3 class="sidebar-title"><span class="emoji-fix">📌</span> <span data-i18n="navHeader">Разделы и Инфо</span></h3>
  327. <div class="sidebar-btn-asym glass-element" onclick="toggleSidebar(); window.scrollTo({top: 0, behavior: 'smooth'}); playAudioCue('menuClick');">🏠 <span data-i18n="navMain">Главная панель</span></div>
  328. <div class="sidebar-btn-asym glass-element" onclick="openTestMenu(); toggleSidebar(); playAudioCue('menuClick');">🎓 <span data-i18n="navTest">Итоговое Тестирование</span></div>
  329. <div class="sidebar-btn-asym glass-element" onclick="showInfoModal('formulas'); toggleSidebar(); playAudioCue('menuClick');">📚 <span data-i18n="navFormulas">Справочник формул</span></div>
  330. <div class="sidebar-btn-asym glass-element" onclick="showInfoModal('about'); toggleSidebar(); playAudioCue('menuClick');">ℹ️ <span data-i18n="navAbout">О платформе</span></div>
  331. </div>
  332. <div class="sidebar-section">
  333. <h3 class="sidebar-title"><span class="emoji-fix">⚡</span> <span data-i18n="lblGraphTools">Инструменты графиков</span></h3>
  334. <div class="glass-controls">
  335. <button id="btnLabelsToggle" class="btn-glass-on active glass-element" onclick="toggleLabels(); playAudioCue('menuClick');"><span data-i18n="lblLabels">Подписи: Вкл</span></button>
  336. <button id="btnGlowToggle" class="btn-glass-on glass-element" style="background: rgba(var(--panel-rgb), 0.3);" onclick="toggleGlow(); playAudioCue('menuClick');"><span data-i18n="lblNeon">Неон: Выкл</span></button>
  337. </div>
  338. <div class="glass-controls">
  339. <button id="btnGridToggle" class="btn-glass-on active glass-element" onclick="toggleGrid(); playAudioCue('menuClick');"><span data-i18n="lblGrid">Сетка: Вкл</span></button>
  340. <button id="btnLissajous" class="btn-glass-on glass-element" style="background: rgba(var(--panel-rgb), 0.3);" onclick="toggleLissajous(); playAudioCue('menuClick');"><span data-i18n="lblLissajous">Лиссажу: Выкл</span></button>
  341. </div>
  342. <div style="margin-top: 10px;">
  343. <label style="font-size:11px; font-weight:bold; display:block;" data-i18n="lblHeightResize">Высота холстов графиков:</label>
  344. <input type="range" id="canvasHeightSlider" min="160" max="450" value="260" oninput="setCanvasHeight(this.value)">
  345. </div>
  346. </div>
  347. <div class="sidebar-section">
  348. <h3 class="sidebar-title"><span class="emoji-fix">🧊</span> <span data-i18n="lblGlass">Жидкое стекло</span></h3>
  349. <div class="glass-controls">
  350. <button id="btnGOn" class="btn-glass-on active glass-element" onclick="setGlassState(true); playAudioCue('menuClick');">Вкл</button>
  351. <button id="btnGOff" class="btn-glass-on glass-element" style="background: rgba(var(--panel-rgb), 0.3);" onclick="setGlassState(false); playAudioCue('menuClick');">Выкл</button>
  352. <button class="btn-glass-auto glass-element btn-shine" onclick="autoGlass(); playAudioCue('menuClick');"><span class="emoji-fix">🤖</span> Авто</button>
  353. </div>
  354. <input type="range" id="glassIntensity" min="0" max="100" value="70" oninput="updateGlassIntensity()">
  355. </div>
  356. <div class="sidebar-section">
  357. <h3 class="sidebar-title"><span class="emoji-fix">🎨</span> <span data-i18n="lblTheme">Оформление</span></h3>
  358. <div class="setting-block">
  359. <label id="lblThemeSelect" data-i18n="lblThemeSelect">Тема оформления:</label>
  360. <select id="themeSelect" class="emoji-fix glass-element" onchange="changeTheme(this.value); playAudioCue('menuClick');"></select>
  361. </div>
  362. <!-- ВОССТАНОВЛЕН БЛОК ОБОЕВ -->
  363. <div class="setting-block">
  364. <label id="lblCustomBg" data-i18n="lblCustomBg">Свои обои:</label>
  365. <div class="file-upload-wrapper glass-element">
  366. <input type="file" accept="image/*" id="bgUpload" class="file-upload-input" onchange="uploadBackground(event)">
  367. <div class="file-upload-btn" data-i18n="btnPickImg">📂 Выбрать картинку</div>
  368. </div>
  369. <button class="btn btn-reset glass-element interactive-hover" style="width: 100%; margin-top: 8px; padding: 10px;" onclick="clearBackground(); playAudioCue('menuClick');"><span class="emoji-fix">🗑</span> <span data-i18n="btnDelBg">Сбросить фон</span></button>
  370. </div>
  371. <div class="setting-block">
  372. <label id="lblBgFilter" data-i18n="lblBgFilter">Фильтр фона:</label>
  373. <select class="emoji-fix glass-element" id="bgFilter" onchange="changeFilter(this.value); playAudioCue('menuClick');"></select>
  374. </div>
  375. </div>
  376. <div style="flex-grow: 1;"></div>
  377. <div style="text-align: center; font-size: 12px; opacity: 0.5;">v19.0 Enterprise Platform</div>
  378. </div>
  379. <!-- ВЕРХНЯЯ ПАНЕЛЬ С КНОПКАМИ ОБНОВИТЬ И ФУЛЛСКРИН -->
  380. <div class="top-bar-wrapper" id="main-nav-bar">
  381. <div class="top-bar glass-panel pop-animation">
  382. <button class="menu-btn-special interactive-hover" onclick="toggleSidebar(); playAudioCue('glassBell');"><span class="emoji-fix">☰</span> <span class="hide-mobile" data-i18n="btnMenu">Меню</span></button>
  383. <button class="btn-reload-styled interactive-hover" onclick="location.reload(); playAudioCue('glassBell');"><span class="emoji-fix">🔄</span> <span class="hide-mobile" data-i18n="btnReload">Обновить</span></button>
  384. <!-- КРУГЛЫЙ ЖИВОЙ ЛОГОТИП -->
  385. <div class="nav-logo interactive-hover" onclick="window.scrollTo({top: 0, behavior: 'smooth'}); cursor: pointer;">
  386. <div class="logo-super-shape small"><div class="logo-inner-content"><span class="emoji-fix" style="font-size:14px;">〽️</span></div></div>
  387. <span class="hide-mobile gradient-text">HarmoniX</span>
  388. </div>
  389. <button class="btn-test-special interactive-hover" onclick="openTestMenu(); playAudioCue('glassBell');"><span class="emoji-fix">🎓</span> <span data-i18n="btnTest">Тесты</span></button>
  390. <button class="btn-fullscreen-compact interactive-hover" onclick="toggleFullscreen(); playAudioCue('glassBell');" title="Full Screen"><span class="emoji-fix">🔲</span></button>
  391. </div>
  392. </div>
  393. <!-- ОСНОВНОЙ КОНТЕНТ И ГРАФИКИ -->
  394. <div id="main-app">
  395. <div class="grid-container">
  396. <div class="panel glass-panel pop-animation" style="animation-delay: 0.05s;">
  397. <div class="panel-header"><h3 style="margin:0; font-size:16px;" data-i18n="pnl1">1. Базовое колебание</h3><span id="lblLisMode" style="font-size:12px; font-weight:bold; color:var(--accent);"></span></div>
  398. <div class="canvas-wrapper glass-element" ondblclick="resetCanvasSizes()">
  399. <canvas id="canvas-single"></canvas>
  400. <div class="chart-telemetry" id="telemSingle">
  401. <div class="telemetry-chip"><span data-i18n="telWave">Волна</span>: <span id="telWaveName" class="val-highlight">V1</span></div>
  402. <div class="telemetry-chip"><span data-i18n="telF">Частота</span>: <span id="telFreq" class="val-highlight">440</span> <span class="unit-freq" data-i18n="hz">Гц</span></div>
  403. <div class="telemetry-chip"><span data-i18n="telA">Амплитуда</span>: <span id="telAmp" class="val-highlight">80</span>%</div>
  404. <div class="telemetry-chip"><span data-i18n="lblPhaseShort">Фаза</span>: <span id="telPhase" class="val-highlight">0°</span></div>
  405. </div>
  406. </div>
  407. </div>
  408. <div class="panel glass-panel pop-animation" style="animation-delay: 0.1s;">
  409. <div class="panel-header">
  410. <h3 style="margin:0; font-size:16px;" data-i18n="pnl3">3. Теорема Фурье (Сумма)</h3>
  411. <button class="btn-download-compact interactive-hover" onclick="downloadGraph('canvas-sum'); playAudioCue('cameraSnap');">
  412. <span class="emoji-fix">📷</span> <span data-i18n="btnSaveCompact">Снимок</span>
  413. </button>
  414. </div>
  415. <div class="canvas-wrapper glass-element" ondblclick="resetCanvasSizes()">
  416. <canvas id="canvas-sum"></canvas>
  417. <div class="chart-telemetry" id="telemSum">
  418. <div class="telemetry-chip"><span data-i18n="telClip">Нелинейное</span>: <span id="telClipStatus" class="val-highlight">Выкл</span></div>
  419. </div>
  420. </div>
  421. </div>
  422. <div class="panel glass-panel pop-animation" style="animation-delay: 0.15s;">
  423. <div class="panel-header">
  424. <h3 style="margin:0; font-size:16px;" data-i18n="pnl2">2. Панель Управления</h3>
  425. <div class="presets-row">
  426. <button class="glass-element btn-shine active interactive-hover" id="pr_sine" onclick="setPreset('sine')"><span class="emoji-fix">〰</span> <span data-i18n="btnSine">Синус</span></button>
  427. <button class="glass-element interactive-hover" id="pr_square" onclick="setPreset('square')"><span class="emoji-fix">🟦</span> <span data-i18n="btnSquare">Меандр</span></button>
  428. <button class="glass-element interactive-hover" id="pr_triangle" onclick="setPreset('triangle')"><span class="emoji-fix">📐</span> <span data-i18n="btnTriangle">Треуг.</span></button>
  429. <button class="glass-element interactive-hover" id="pr_sawtooth" onclick="setPreset('sawtooth')"><span class="emoji-fix">🔪</span> <span data-i18n="btnSaw">Пила</span></button>
  430. </div>
  431. </div>
  432. <div class="tabs">
  433. <div class="tab glass-element active interactive-hover" onclick="switchTab(0); playAudioCue('mainPulse');">V1</div>
  434. <div class="tab glass-element interactive-hover" onclick="switchTab(1); playAudioCue('mainPulse');">V2</div>
  435. <div class="tab glass-element interactive-hover" onclick="switchTab(2); playAudioCue('mainPulse');">V3</div>
  436. </div>
  437. <div class="control-row">
  438. <div class="control-label" data-i18n="lblStep">Шаг частоты:</div>
  439. <div class="control-body">
  440. <div class="step-toggle">
  441. <div class="step-btn interactive-hover" id="st10" onclick="updateStep(10); playAudioCue('mainPulse');">10 <span class="unit-freq" data-i18n="hz">Гц</span></div>
  442. <div class="step-btn active interactive-hover" id="st100" onclick="updateStep(100); playAudioCue('mainPulse');">100 <span class="unit-freq" data-i18n="hz">Гц</span></div>
  443. </div>
  444. </div>
  445. </div>
  446. <div class="control-row">
  447. <div class="control-label" data-i18n="lblFreq">Частота (Гц):</div>
  448. <div class="control-body">
  449. <input type="range" id="freq" min="0" max="20000" step="100" value="440" oninput="updateWaveFromSlider('freq')">
  450. <div class="input-wrapper"><input type="number" id="inputV" class="value-input glass-element" value="440" min="0" max="20000" oninput="updateWaveFromInput('freq')"></div>
  451. </div>
  452. </div>
  453. <div class="control-row">
  454. <div class="control-label" data-i18n="lblAmp">Амплитуда (%):</div>
  455. <div class="control-body">
  456. <input type="range" id="amp" min="0" max="100" value="80" oninput="updateWaveFromSlider('amp')">
  457. <div class="input-wrapper"><input type="number" id="inputA" class="value-input glass-element" value="80" min="0" max="100" oninput="updateWaveFromInput('amp')"></div>
  458. </div>
  459. </div>
  460. <div class="secondary-params glass-element">
  461. <div class="control-row" style="margin-bottom: 8px;">
  462. <div class="control-label"><span data-i18n="lblPhase">Сдвиг фазы (°)</span><span class="hint-icon" data-i18n-hint="hintPhase" data-hint="Сдвиг фазы смещает волну во времени. Сдвиг на 180° создает инвертированную волну (противофазу).">?</span></div>
  463. <div class="control-body">
  464. <input type="range" class="slider-alt" id="phase" min="0" max="360" step="1" value="0" oninput="updateWaveFromSlider('phase')">
  465. <div class="input-wrapper"><input type="number" id="inputP" class="value-input input-alt glass-element" value="0" min="0" max="360" oninput="updateWaveFromInput('phase')"></div>
  466. </div>
  467. </div>
  468. <div class="control-row" style="margin-bottom: 0;">
  469. <div class="control-label"><span data-i18n="lblSpeed">Скорость волны</span><span class="hint-icon" data-i18n-hint="hintSpeedGraph" data-hint="Управление скоростью анимации на графиках. Не меняет физическую частоту колебаний.">?</span></div>
  470. <div class="control-body">
  471. <input type="range" class="slider-alt" id="speed" min="0.1" max="3" step="0.1" value="1" oninput="updateSpeedValue(this.value);">
  472. <div class="input-wrapper"><div class="value-input input-alt glass-element" id="valSpeedDisp" style="line-height: 20px;">1.0x</div></div>
  473. </div>
  474. </div>
  475. </div>
  476. <div class="btn-group">
  477. <button class="btn btn-audio interactive-hover" onclick="toggleAudio(); playAudioCue('mainPulse');" id="btnAudio"><span class="emoji-fix">🔊</span> <span data-i18n="btnAudio">Динамик</span></button>
  478. <button class="btn btn-pause interactive-hover" onclick="togglePause(); playAudioCue('mainPulse');" id="btnPause"><span class="emoji-fix">⏸</span> <span data-i18n="btnPause">Стоп-кадр</span></button>
  479. <button class="btn btn-dist interactive-hover" onclick="toggleClip(); playAudioCue('mainPulse');" id="btnClip" style="position: relative; display: flex; align-items: center; justify-content: center; gap: 4px;">
  480. <span class="emoji-fix">⚡</span>
  481. <span data-i18n="btnClip">Нелинейн. преобр.</span>
  482. <span class="hint-icon pulse-alert" style="background: #E91E63; border-color: #fff; box-shadow: 0 0 12px #E91E63; width: 18px; height: 18px; font-size: 11px;" data-i18n-hint="hintClip" data-hint="Имитирует перегрузку усилителя (клиппинг).">?</span>
  483. </button>
  484. <button class="btn btn-reset interactive-hover" onclick="resetAll(); playAudioCue('mainPulse');"><span class="emoji-fix">🔄</span> <span data-i18n="btnReset">Сброс</span></button>
  485. </div>
  486. </div>
  487. <div class="panel glass-panel pop-animation" style="animation-delay: 0.2s;">
  488. <h3 style="margin:0; margin-bottom: 8px; font-size:16px;" data-i18n="pnl4">4. Спектрограмма</h3>
  489. <div class="canvas-wrapper glass-element" ondblclick="resetCanvasSizes()">
  490. <canvas id="canvas-spectrum"></canvas>
  491. <div class="chart-telemetry" id="telemSpec">
  492. <div class="telemetry-chip"><span data-i18n="telXAxis">Ось X</span>: <span class="val-highlight">0 — 20k <span class="unit-freq" data-i18n="hz">Гц</span></span></div>
  493. <div class="telemetry-chip"><span data-i18n="telPeaks">Пики</span>: <span id="telSpecPeaks" class="val-highlight">1</span></div>
  494. </div>
  495. </div>
  496. </div>
  497. <div class="panel glass-panel pop-animation" style="grid-column: 1 / -1; border-color: var(--tzi-color); animation-delay: 0.25s;">
  498. <div class="panel-header">
  499. <h3 style="color: var(--tzi-color); margin: 0;"><span class="emoji-fix">🛡️</span> <span data-i18n="tziTitle">ТЗИ: Виброакустическая защита</span></h3>
  500. <span class="hint-icon" style="background:var(--tzi-color); color:#000; margin: 0;" data-i18n-hint="hintTzi" data-hint="Симуляция технической защиты информации.">?</span>
  501. </div>
  502. <p style="font-size:15px; margin-bottom:15px; margin-top: 10px; font-weight: bold;" data-i18n="tziDesc">Активация генератора шума для маскировки речевой информации от средств перехвата:</p>
  503. <div style="display:flex; justify-content:center;">
  504. <button class="btn btn-tzi interactive-hover" id="btnTziMode" style="width:100%; max-width:400px; padding:14px; font-size:16px; font-weight:900;" onclick="toggleTZI()"><span class="emoji-fix">🔒</span> <span data-i18n="btnTziStart">Активировать маскировку</span></button>
  505. </div>
  506. </div>
  507. </div>
  508. <!-- ВОССТАНОВЛЕНЫ ОБУЧАЮЩИЕ БЛОКИ И РЕСУРСЫ -->
  509. <div class="site-content glass-panel pop-animation" style="animation-delay: 0.3s;">
  510. <h2 class="content-header gradient-text" data-i18n="eduHeader">📚 Академия Волновой Механики</h2>
  511. <div class="info-grid">
  512. <div class="info-card glass-element interactive-hover">
  513. <h3 data-i18n="eduCard1Title"><span class="emoji-fix">〰️</span> Природа механических волн</h3>
  514. <p data-i18n="eduCard1Text">Механическая волна представляет собой процесс распространения колебательного возмущения в упругой среде. При этом частицы среды лишь совершают малые колебания возле равновесия, а переносится исключительно <b>ЭНЕРГИЯ И ИМПУЛЬС</b>.</p>
  515. </div>
  516. <div class="info-card glass-element interactive-hover">
  517. <h3 data-i18n="eduCard2Title"><span class="emoji-fix">📉</span> Теорема и спектральный анализ Фурье</h3>
  518. <p data-i18n="eduCard2Text1">В 1822 году Жан-Батист Фурье доказал: <b>ЛЮБОЙ ПЕРИОДИЧЕСКИЙ СИГНАЛ</b> сложной формы может быть разложен в ряд простых гармонических волн кратных частот.</p>
  519. </div>
  520. </div>
  521. <!-- УМНЫЙ КАЛЬКУЛЯТОР ДЛИНЫ ВОЛНЫ С ВЫПАДАЮЩИМ СПИСКОМ -->
  522. <div class="calc-grid">
  523. <div class="wave-calculator">
  524. <h3 style="margin-top:0; color:#fff;" data-i18n="calcTitle"><span class="emoji-fix">🧮</span> Калькулятор длины волны</h3>
  525. <div class="calc-screen">
  526. <div class="calc-formula">λ = v / ν</div>
  527. <div style="font-size: 13px; color: #81c784; margin-bottom: 5px;" id="lblCalcAirDesc" data-i18n="calcAirDesc">В воздухе (343 м/с):</div>
  528. <div class="calc-result"><span id="calcResAir">0.77</span> <span class="calc-m" data-i18n="meters">м</span></div>
  529. </div>
  530. <div class="calc-controls">
  531. <span style="font-weight: bold; font-size: 14px;"><span data-i18n="lblFreqShort">ν (Гц)</span>:</span>
  532. <input type="number" id="calcFreq1" class="calc-input" value="440" min="1" oninput="calculateWaveLength(); playAudioCue('calcKey');">
  533. </div>
  534. <div style="display:flex; justify-content:center; gap:8px; align-items: center; border-top: 1px dashed #444; padding-top: 10px; margin-top: 10px; flex-wrap: wrap;">
  535. <span style="font-weight: bold; font-size:13px; color:#aaa;" data-i18n="calcSpeedLabel">Скорость (м/с):</span>
  536. <input type="number" id="calcSpeedCustom" class="calc-input" style="width: 70px; padding: 6px; font-size: 14px;" value="343" step="10" oninput="onManualSpeedInput(); calculateWaveLength(); playAudioCue('calcKey');">
  537. <select id="mediumSelect" class="calc-select" onchange="onSelectMedium(); calculateWaveLength(); playAudioCue('calcKey');"></select>
  538. <span class="hint-icon" style="background:#888;" data-i18n-hint="hintSpeed" data-hint="Выберите среду из списка">?</span>
  539. </div>
  540. </div>
  541. <div class="wave-calculator">
  542. <h3 style="margin-top:0; color:#fff;" data-i18n="calc2Title"><span class="emoji-fix">⏱</span> Калькулятор Периода</h3>
  543. <div class="calc-screen">
  544. <div class="calc-formula">T = 1 / ν | ω = 2πν</div>
  545. <div style="font-size: 13px; color: #81c784; margin-bottom: 5px;" data-i18n="calcCycle">Длительность 1 цикла (Период):</div>
  546. <div class="calc-result"><span id="calcResPeriod">0.00227</span> <span class="calc-s" data-i18n="sec">с</span></div>
  547. </div>
  548. <div class="calc-controls">
  549. <span style="font-weight: bold; font-size: 14px;"><span data-i18n="lblFreqShort">ν (Гц)</span>:</span>
  550. <input type="number" id="calcFreq2" class="calc-input" value="440" min="1" oninput="document.getElementById('calcFreq1').value = this.value; calculateWaveLength(); playAudioCue('calcKey');">
  551. </div>
  552. <div style="display:flex; justify-content: space-around; margin-top: 15px; font-family: monospace; color: #81c784; font-size: 13px; background: rgba(0,0,0,0.5); padding: 8px; border-radius: 8px;">
  553. <div id="calcResOmega" data-i18n="omegaLabel">Цикл. частота (ω): ~2764.6 рад/с</div>
  554. </div>
  555. </div>
  556. </div>
  557. <!-- FAQ И РЕСУРСЫ -->
  558. <h2 class="content-header gradient-text" style="margin-top:35px;" data-i18n="faqTitle">💬 Динамические вопросы и ответы (FAQ)</h2>
  559. <div id="dynamic-faq-container"></div>
  560. <h3 data-i18n="resTitleRU" style="text-align: center; font-size: 20px; margin-top: 45px; color: var(--accent);">🇷🇺 Российские образовательные платформы</h3>
  561. <div class="resource-grid">
  562. <a href="https://foxford.ru/wiki/fizika" target="_blank" class="premium-link-btn foxford-btn interactive-hover" onclick="playAudioCue('glassBell');">
  563. <span class="emoji-fix" style="font-size:18px;">🦊</span>
  564. <span data-i18n="resFoxford">Фоксфорд.Учебник</span>
  565. </a>
  566. <a href="https://interneturok.ru/subject/physics" target="_blank" class="premium-link-btn ru-btn interactive-hover" onclick="playAudioCue('glassBell');">
  567. <span class="emoji-fix" style="font-size:18px;">📚</span>
  568. <span data-i18n="resInterneturok">InternetUrok: Физика</span>
  569. </a>
  570. <a href="http://www.virtulab.net/" target="_blank" class="premium-link-btn ru-btn interactive-hover" onclick="playAudioCue('glassBell');">
  571. <span class="emoji-fix" style="font-size:18px;">🏫</span>
  572. <span data-i18n="resVirtulab">VirtuLab: Лаборатории</span>
  573. </a>
  574. <a href="https://rutube.ru/search/?q=%D0%9F%D0%B0%D0%B2%D0%B5%D0%BB+%D0%92%D0%B8%D0%BA%D1%82%D0%BE%D1%80+%D0%BC%D0%B5%D1%85%D0%B0%D0%BD%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B5+%D0%B2%D0%BE%D0%BB%D0%BD%D1%8B" target="_blank" class="premium-link-btn rutube-btn interactive-hover" onclick="playAudioCue('glassBell');">
  575. <span class="emoji-fix" style="font-size:18px;">⬛</span>
  576. <span data-i18n="linkRutube">Rutube: Лекции по физике</span>
  577. </a>
  578. <a href="https://vk.com/video/search?q=%D0%9F%D0%B0%D0%B2%D0%B5%D0%BB+%D0%92%D0%B8%D0%BA%D1%82%D0%BE%D1%80+%D0%BC%D0%B5%D1%85%D0%B0%D0%BD%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B5+%D0%B2%D0%BE%D0%BB%D0%BD%D1%8B" target="_blank" class="premium-link-btn ru-btn interactive-hover" onclick="playAudioCue('glassBell');">
  579. <span class="emoji-fix" style="font-size:18px;">🟦</span>
  580. <span data-i18n="linkVK">VK Видео: Волны и колебания</span>
  581. </a>
  582. </div>
  583. <h3 data-i18n="resTitleINT" style="text-align: center; font-size: 20px; margin-top: 35px; color: var(--accent);">🌍 Международные симуляторы и материалы</h3>
  584. <div class="resource-grid">
  585. <a href="https://www.youtube.com/results?search_query=%D0%9F%D0%B0%D0%B2%D0%B5%D0%BB+%D0%92%D0%B8%D0%BA%D1%82%D0%BE%D1%80+%D0%BC%D0%B5%D1%85%D0%B0%D0%BD%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B5+%D0%B2%D0%BE%D0%BB%D0%BD%D1%8B" target="_blank" class="premium-link-btn youtube-btn interactive-hover" onclick="playAudioCue('glassBell');">
  586. <span class="emoji-fix" style="font-size:18px;">🟥</span>
  587. <span data-i18n="linkYT">YouTube: Урок Павла Виктора</span>
  588. </a>
  589. <a href="https://phet.colorado.edu/sims/html/wave-on-a-string/latest/wave-on-a-string_all.html" target="_blank" class="premium-link-btn sim-btn interactive-hover" onclick="playAudioCue('glassBell');">
  590. <span class="emoji-fix" style="font-size:18px;">🧪</span>
  591. <span data-i18n="resPhet">PhET: Волна на струне</span>
  592. </a>
  593. <a href="https://academo.org/demos/wave-interference-beat-frequency/" target="_blank" class="premium-link-btn sim-btn interactive-hover" onclick="playAudioCue('glassBell');">
  594. <span class="emoji-fix" style="font-size:18px;">📡</span>
  595. <span data-i18n="resAcademo">Academo: Интерференция</span>
  596. </a>
  597. <a href="https://www.falstad.com/fourier/" target="_blank" class="premium-link-btn sim-btn interactive-hover" onclick="playAudioCue('glassBell');">
  598. <span class="emoji-fix" style="font-size:18px;">📈</span>
  599. <span data-i18n="resFalstad">Falstad: Синтезатор</span>
  600. </a>
  601. </div>
  602. </div>
  603. <div class="app-footer">
  604. HarmoniX v19.0 Enterprise | Лаборатория Волновой Физики © 2026<br>
  605. Интерактивный анализ механических волн, Теория Фурье и Фигуры Лиссажу
  606. </div>
  607. </div>
  608. <!-- ТЕСТИРОВАНИЕ: СЛАЙДЕР ВОПРОСОВ И УМНЫЙ ТАЙМЕР -->
  609. <div id="test-overlay" class="modal-overlay">
  610. <div class="modal-content glass-panel" style="z-index: 99999;">
  611. <span class="close-btn" onclick="closeTest(); playAudioCue('mainPulse');">&times;</span>
  612. <h2 class="gradient-text" style="text-align: center; margin-top: 0; font-size: 24px;"><span class="emoji-fix">📝</span> <span data-i18n="testHeader">Тестирование знаний</span></h2>
  613. <div id="test-start-menu">
  614. <p style="text-align:center; font-size: 15px; margin-bottom:15px; font-weight: bold;" data-i18n="testSelectSub">Выберите режим тестирования:</p>
  615. <div class="test-modes" id="testModesContainer">
  616. <button class="btn btn-easy interactive-hover" onclick="startQuiz(5)"><span class="emoji-fix">🟢</span> <span data-i18n="modeExp">Экспресс</span><br><small data-i18n="time5">5 (Без таймера)</small></button>
  617. <button class="btn btn-medium interactive-hover" onclick="startQuiz(15)"><span class="emoji-fix">⚡</span> <span data-i18n="modeStd">Стандарт</span><br><small data-i18n="time15">15 (5 мин)</small></button>
  618. <button class="btn btn-hard interactive-hover" onclick="startQuiz(30)"><span class="emoji-fix">🔥</span> <span data-i18n="modeFull">Полный курс</span><br><small data-i18n="time30">30 (10 мин)</small></button>
  619. <button class="btn btn-ultra interactive-hover" onclick="startQuiz(50)"><span class="emoji-fix">👑</span> <span data-i18n="modeUltra">Ультра Профи</span><br><small data-i18n="time50">50 (15 мин)</small></button>
  620. </div>
  621. </div>
  622. <!-- ИНТЕРФЕЙС ТЕСТА -->
  623. <div id="quiz-process" style="display: none;">
  624. <div class="quiz-timer-pill glass-panel btn-shine" id="quizTimerBar" style="display:none;">
  625. <div class="timer-progress-bg">
  626. <div id="quizTimerProgress" class="timer-progress-fill"></div>
  627. </div>
  628. <div class="timer-content">
  629. <div style="display:flex; gap: 8px; align-items:center;">
  630. <span class="timer-display" id="quizTimerVal">00:00</span>
  631. </div>
  632. <div style="display:flex; gap: 6px;">
  633. <button class="btn-submit-main interactive-hover" onclick="checkTest(); playAudioCue('glassBell');" style="padding: 6px 14px; font-size:13px;"><span class="emoji-fix">✅</span> <span class="hide-mobile" data-i18n="btnCheck">Сдать</span></button>
  634. <button class="btn btn-reset interactive-hover" onclick="closeTest(); playAudioCue('mainPulse');" style="padding: 6px 14px; font-size:13px; border:1px solid #fff;"><span class="emoji-fix">🚪</span> <span class="hide-mobile" data-i18n="btnExit">Выйти</span></button>
  635. </div>
  636. </div>
  637. </div>
  638. <div class="question-card-wrapper" id="quiz-card-wrapper">
  639. <div id="quiz-container"></div>
  640. </div>
  641. <div class="quiz-nav-row" id="quiz-nav-controls">
  642. <button class="quiz-nav-btn interactive-hover" id="btnPrevQ" onclick="prevQuestion(); playAudioCue('mainPulse');" disabled>
  643. ◀ <span data-i18n="btnPrev">Назад</span>
  644. </button>
  645. <span id="quiz-step-indicator" style="font-weight: 800; font-size: 15px; color: var(--accent);">1 / 5</span>
  646. <button class="quiz-nav-btn interactive-hover" id="btnNextQ" onclick="nextQuestion(); playAudioCue('mainPulse');">
  647. <span data-i18n="btnNext">Вперед</span> ▶
  648. </button>
  649. </div>
  650. <!-- ЭКРАН РЕЗУЛЬТАТОВ (АНИМАЦИЯ SVG КРУГА) -->
  651. <div id="test-result-screen">
  652. <div class="score-circle-wrapper">
  653. <svg width="140" height="140" viewBox="0 0 120 120" class="circular-progress">
  654. <circle class="bg" cx="60" cy="60" r="54"></circle>
  655. <circle class="progress" id="scoreProgress" cx="60" cy="60" r="54"></circle>
  656. </svg>
  657. <div class="score-text" id="scoreCircleText">0%</div>
  658. </div>
  659. <div id="test-result-text" style="font-size: 20px; font-weight: 900; margin-bottom: 15px;"></div>
  660. <div class="review-box" id="quiz-explanation"></div>
  661. <div class="test-controls" style="margin-top: 25px;">
  662. <button class="btn btn-test-special interactive-hover" onclick="openTestMenu(); playAudioCue('glassBell');" style="font-size: 15px; padding: 12px 24px;">
  663. <span class="emoji-fix">🔄</span> <span data-i18n="btnTryAgain">Пройти снова</span>
  664. </button>
  665. <button class="btn btn-reset interactive-hover" onclick="closeTest(); playAudioCue('mainPulse');" style="font-size: 15px; padding: 12px 24px;">
  666. <span class="emoji-fix">🚪</span> <span data-i18n="btnExit">Выйти</span>
  667. </button>
  668. </div>
  669. </div>
  670. </div>
  671. </div>
  672. </div>
  673. <!-- ИНФО ОВЕРЛЕИ -->
  674. <div id="info-overlay" class="modal-overlay">
  675. <div class="modal-content glass-panel" style="z-index: 99999;">
  676. <span class="close-btn" onclick="closeInfoModal(); playAudioCue('mainPulse');">&times;</span>
  677. <!-- ПАПКА ФОРМУЛ С ДОП ССЫЛКАМИ -->
  678. <div id="modal-formulas" class="info-content" style="display:none;">
  679. <h2 class="gradient-text" style="text-align:center;"><span class="emoji-fix">📚</span> <span data-i18n="modalFormulasTitle">Справочник формул</span></h2>
  680. <div class="formula-grid">
  681. <div class="formula-card pop-animation interactive-hover" data-i18n="form1"><b>Уравнение бегущей волны:</b><br> y(x,t) = A · cos(w·t - k·x + θ)</div>
  682. <div class="formula-card pop-animation interactive-hover" data-i18n="form2"><b>Гармоническое колебание:</b><br> y(t) = A · cos(w·t + θ)</div>
  683. <div class="formula-card pop-animation interactive-hover" data-i18n="form3"><b>Период и частота:</b><br> T = 1 / V</div>
  684. <div class="formula-card pop-animation interactive-hover" data-i18n="form4"><b>Циклическая частота:</b><br> w = 2 · π · V</div>
  685. <div class="formula-card pop-animation interactive-hover" data-i18n="form5"><b>Длина волны:</b><br> λ = v / V = v · T</div>
  686. <div class="formula-card pop-animation interactive-hover" data-i18n="form6"><b>Волновое число:</b><br> k = 2 · π / λ</div>
  687. <div class="formula-card pop-animation interactive-hover" data-i18n="form7"><b>Эффект Доплера:</b><br> f' = f · (v ± v₀) / (v ∓ vₛ)</div>
  688. <div class="formula-card pop-animation interactive-hover" data-i18n="form8"><b>Акустический импеданс:</b><br> Z = ρ · v</div>
  689. </div>
  690. <h3 style="text-align:center; color:var(--accent); margin-top:25px;" data-i18n="fConst">⚛️ Фундаментальные константы</h3>
  691. <div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap:12px; margin-top:10px;">
  692. <div class="formula-card pop-animation interactive-hover" style="text-align:center; padding: 14px;" onclick="playAudioCue('calcKey')">
  693. <b data-i18n="c1">Скорость света (c)</b>
  694. <span style="color:#00E676; font-size:18px;">3·10⁸ м/с</span>
  695. </div>
  696. <div class="formula-card pop-animation interactive-hover" style="text-align:center; padding: 14px;" onclick="playAudioCue('calcKey')">
  697. <b data-i18n="c2">Скорость звука (v)</b>
  698. <span style="color:#00E676; font-size:18px;">~343 м/с</span>
  699. </div>
  700. <div class="formula-card pop-animation interactive-hover" style="text-align:center; padding: 14px;" onclick="playAudioCue('calcKey')">
  701. <b data-i18n="c3">Число Пи (π)</b>
  702. <span style="color:#00E676; font-size:18px;">3.14159...</span>
  703. </div>
  704. </div>
  705. <div style="margin-top:20px; text-align:center;">
  706. <a href="http://hyperphysics.phy-astr.gsu.edu/hbase/hph.html" target="_blank" style="color:var(--base-wave); font-weight:bold; text-decoration:none;" data-i18n="hyperPhysics">🔗 База знаний HyperPhysics (GSU)</a>
  707. </div>
  708. </div>
  709. <!-- РАСШИРЕННЫЙ РАЗДЕЛ О ПРОЕКТЕ -->
  710. <div id="modal-about" class="info-content" style="display:none;">
  711. <h2 class="gradient-text" style="text-align:center;"><span class="emoji-fix">🚀</span> <span data-i18n="modalAboutTitle">О платформе HarmoniX</span></h2>
  712. <div class="about-section pop-animation interactive-hover">
  713. <h3 data-i18n="aboutDescTitle"><span class="emoji-fix">ℹ️</span> Философия проекта</h3>
  714. <p data-i18n="aboutDescText">HarmoniX — это высокотехнологичная PWA-среда для визуального моделирования волновых процессов, гармонического сложения и анализа спектра Фурье.</p>
  715. </div>
  716. <h3 data-i18n="aboutHistoryTitle" style="color: var(--accent); margin-top:20px;">📜 Журнал изменений (Changelog)</h3>
  717. <div class="timeline">
  718. <div class="timeline-item pop-animation interactive-hover">
  719. <b>v19.0 Enterprise Grand Master:</b> Интерактивный SVG-круг прогресса тестирования с анимацией. "Живые" ссылки на Wikipedia в ответах. Интерактивный логотип. Новые формулы Доплера.
  720. </div>
  721. <div class="timeline-item pop-animation interactive-hover">
  722. <b>v18.5 Enterprise UX Update:</b> Волновая форма кнопок (18px 6px). Умная лента таймера с градиентами. Пошаговый слайдер тестов (без скролла).
  723. </div>
  724. <div class="timeline-item pop-animation interactive-hover">
  725. <b>v18.0 Security Addition:</b> Интеграция модуля ТЗИ (Акустическая маскировка), генератор белого шума. 100% перевод интерфейса (i18n). Умный селектор сред.
  726. </div>
  727. <div class="timeline-item pop-animation interactive-hover">
  728. <b>v16.0 Ultimate:</b> Двуязычная адаптация, раздельный WebAudio, устранение конфликтов canvas, замена SVG на системные эмодзи.
  729. </div>
  730. <div class="timeline-item pop-animation interactive-hover">
  731. <b>v1.0 - 12.0:</b> Генератор вопросов, Фигуры Лиссажу, базовый синтезатор Фурье.
  732. </div>
  733. </div>
  734. </div>
  735. </div>
  736. </div>
  737. <script>
  738. let currentLang = 'ru';
  739. let currentPreset = 'sine';
  740. let waves = [{ f: 440, a: 80, p: 0 }, { f: 880, a: 0, p: 0 }, { f: 1320, a: 0, p: 0 }];
  741. let currentWave = 0, freqStep = 100, time = 0, animSpeedMultiplier = 1.0;
  742. let isClipped = false, isAudioOn = false, isPaused = false;
  743. let isGlassOn = true, isGlowOn = false, isGridOn = true, isLissajous = false, showLabels = true;
  744. let waveCtx = null, uiCtx = null, oscillators = [];
  745. let animationFrameId = null, cachedBaseColor = '#2196F3';
  746. const BASE_ANIM_SPEED = 0.08;
  747. let isMaskingTZI = false, noiseSource = null;
  748. const i18n = {
  749. ru: {
  750. appTitle: "HarmoniX", appSubtitle: "Интерактивная Лаборатория Волновой Физики", btnStart: "Начать работу",
  751. menuSettings: "Настройки", lblGraphTools: "Инструменты графиков", lblLabels: "Подписи",
  752. lblNeon: "Неон", lblGrid: "Сетка", lblLissajous: "Лиссажу", lblGlass: "Жидкое стекло",
  753. lblPresets: "Пресеты", btnExport: "Экспорт", btnImport: "Импорт", lblTheme: "Оформление",
  754. lblThemeSelect: "10 ТЕМ ОФОРМЛЕНИЯ:", lblCustomBg: "Свои обои:", lblBgFilter: "Фильтр фона:",
  755. btnDelBg: "Сбросить фон", btnMenu: "Меню", btnReload: "Обновить", btnTest: "Тесты", btnFull: "На весь экран", btnExitFull: "Выйти",
  756. pnl1: "1. Базовое колебание", pnl2: "2. Панель Управления", pnl3: "3. Теорема Фурье (Сумма)", pnl4: "4. Спектрограмма",
  757. lblStep: "Шаг частоты:", lblFreq: "Частота (Гц):", lblAmp: "Амплитуда (%):", lblPhase: "Сдвиг фазы (°):", lblSpeed: "Скорость волны:",
  758. btnAudio: "Динамик", btnMute: "Выкл. Звук", btnPause: "Стоп-кадр", btnPlay: "Запуск", btnClip: "Нелинейн. преобр.", btnUnclip: "Выкл. преобр.", btnReset: "Сброс",
  759. btnSine: "Синус", btnSquare: "Меандр", btnTriangle: "Треуг.", btnSaw: "Пила", btnSave: "Сохранить", btnSaveCompact: "Снимок",
  760. eduHeader: "📚 Академия Волновой Механики", eduCard1Title: "〰️ Природа механических волн",
  761. eduCard1Text: "Механическая волна представляет собой процесс распространения колебательного возмущения в упругой среде. При этом частицы среды лишь совершают малые колебания возле равновесия, а переносится исключительно <b>ЭНЕРГИЯ И ИМПУЛЬС</b>.",
  762. eduCard1Params: "Ключевые физические параметры:", eduParam1: "• <b>Период (T):</b> длительность одного полного колебания.", eduParam2: "• <b>Частота (V):</b> число полных циклов за 1 секунду.", eduParam3: "• <b>Циклическая частота (w):</b> угловая скорость изменения фазы.", eduParam4: "• <b>Сдвиг фазы (θ):</b> начальное положение фазового угла.",
  763. eduCard2Title: "📉 Теорема и спектральный анализ Фурье", eduCard2Text1: "В 1822 году Жан-Батист Фурье доказал: <b>ЛЮБОЙ ПЕРИОДИЧЕСКИЙ СИГНАЛ</b> сложной формы может быть разложен в ряд простых гармонических волн кратных частот.",
  764. eduCard2Text2: "Это фундаментальная база цифровой обработки сигналов: от синтезаторов звука и сжатия MP3 до радиопередачи Wi-Fi и сетей 5G.",
  765. calcTitle: "🧮 Калькулятор длины волны", calc2Title: "⏱ Калькулятор Периода", calcCustomMedium: "Свой вариант...",
  766. faqTitle: "💬 Динамические вопросы и ответы (FAQ)",
  767. resTitleRU: "🇷🇺 Российские образовательные платформы",
  768. resTitleINT: "🌍 Международные симуляторы и материалы",
  769. resPhet: "PhET: Волна на струне", resAcademo: "Academo: Интерференция", resFalstad: "Falstad: Синтезатор",
  770. resVirtulab: "VirtuLab: Лаборатории", resInterneturok: "InternetUrok: Физика", resFoxford: "Фоксфорд.Учебник",
  771. testHeader: "Тестирование знаний", testSelectSub: "Выберите режим тестирования:", btnCheck: "Сдать",
  772. modeExp: "Экспресс", time5: "5 (Без таймера)", modeStd: "Стандарт", time15: "15 (5 мин)", modeFull: "Полный курс", time30: "30 (10 мин)", modeUltra: "Ультра Профи", time50: "50 (15 мин)", btnExit: "Выйти", btnTryAgain: "Пройти снова",
  773. telWave: "Волна", telF: "Частота", telA: "Амплитуда", telClip: "Нелинейное", telXAxis: "Ось X", clipOn: "Вкл", clipOff: "Выкл",
  774. lblTelPhase: "Фаза", telY: "Смещение", telSumY: "Σy", telPeaks: "Пики", lblTimeLeft: "Осталось",
  775. navHeader: "Разделы и Инфо", navMain: "Главная панель", navTest: "Итоговое Тестирование", navFormulas: "Справочник формул", navAbout: "О проекте HarmoniX",
  776. lblHeightResize: "Высота холстов графиков:", modalFormulasTitle: "Справочник формул", modalAboutTitle: "О платформе HarmoniX",
  777. modalExtLinks: "🎥 Рекомендованные видеоматериалы:", linkRutube: "Rutube: Лекции по физике", linkVK: "VK Видео: Волны и колебания", linkYT: "YouTube: Лекция Павла Виктора", linkWiki1: "Ряд Фурье (Википедия)",
  778. aboutDescTitle: "ℹ️ Философия проекта", aboutDescText: "HarmoniX — интерактивная вычислительная лаборатория для визуального моделирования волновых процессов, гармонического сложения и анализа спектра Фурье.",
  779. aboutHistoryTitle: "📜 Журнал изменений (Changelog)",
  780. form1: "<b>Уравнение бегущей волны:</b><br> y(x,t) = A · cos(w·t - k·x + θ)", form2: "<b>Гармоническое колебание:</b><br> y(t) = A · cos(w·t + θ)",
  781. form3: "<b>Период и частота:</b><br> T = 1 / V", form4: "<b>Циклическая частота:</b><br> w = 2 · π · V", form5: "<b>Длина волны:</b><br> λ = v / V = v · T",
  782. form6: "<b>Волновое число:</b><br> k = 2 · π / λ", form7: "<b>Эффект Доплера:</b><br> f' = f · (v ± v₀) / (v ∓ vₛ)", form8: "<b>Акустический импеданс:</b><br> Z = ρ · v",
  783. themes: { default: "⚪ Светлая (Базовая)", dark: "⚫ Темная (Ночная)", neon: "🟣 Неон (Киберпанк)", ocean: "🌊 Океан (Глубокая)", forest: "🌲 Лес (Природная)", sunset: "🌅 Закат (Теплая)", cyber: "🟡 Кибер (Желтая)", coffee: "☕ Латте (Кофе)", space: "🌌 Глубокий Космос", sakura: "🌸 Сакура (Розовая)" },
  784. filters: { none: "🚫 Без фильтра", "blur(14px) saturate(140%)": "🌫 Глубокое размытие", "invert(100%)": "💡 Негатив / Инверсия", "contrast(150%) brightness(85%)": "🎬 Кинематограф", "sepia(100%)": "📜 Сепия (Винтаж)" },
  785. hz: "Гц", meters: "м", sec: "с",
  786. tziTitle: "ТЗИ: Виброакустическая защита",
  787. tziDesc: "Активация генератора шума для маскировки речевой информации от средств перехвата:",
  788. btnTziStart: "Активировать маскировку", btnTziStop: "Отключить маскировку",
  789. calcAirDesc: "Среда: Воздух (20°C)", calcSpeedLabel: "Скорость (м/с):", calcCycle: "Длительность 1 цикла (Период):",
  790. hintYellow: "Ищите кружки [?] для подсказок!", btnPickImg: "📂 Выбрать картинку",
  791. testTimeUp: "⏰ Время вышло! Ответы проверены автоматически.",
  792. testReviewTitle: "Детальный разбор вопросов:", testSkipped: "Пропущен",
  793. testResTitle: "Итоговый результат:", testResOf: "из",
  794. testRes90: "🎉 Превосходно! Уровень эксперта.", testRes60: "👍 Хорошо, курс освоен.", testResLow: "😅 Рекомендуем повторить теорию на сайте.",
  795. omegaLabel: "Цикл. частота (ω):", radS: "рад/с", lblFreqShort: "ν (Гц)", lblPhaseShort: "Фаза",
  796. btnPrev: "Назад", btnNext: "Вперед", qOf: "из", qWord: "Вопрос", wikiLink: "Читать подробнее в Wikipedia", hyperPhysics: "🔗 База знаний HyperPhysics (GSU)",
  797. fConst: "⚛️ Фундаментальные константы", c1: "Скорость света (c)", c2: "Скорость звука (v)", c3: "Число Пи (π)", btnPickImg: "📂 Выбрать картинку", btnDelBg: "Сбросить фон"
  798. },
  799. en: {
  800. appTitle: "HarmoniX", appSubtitle: "Interactive Wave Physics Laboratory", btnStart: "Start Analysis",
  801. menuSettings: "Settings", lblGraphTools: "Graph Tools", lblLabels: "Labels",
  802. lblNeon: "Neon Glow", lblGrid: "Grid", lblLissajous: "Lissajous", lblGlass: "Liquid Glass",
  803. lblPresets: "Presets", btnExport: "Export", btnImport: "Import", lblTheme: "Themes",
  804. lblThemeSelect: "10 COLOR THEMES:", lblCustomBg: "Custom Wallpaper:", lblBgFilter: "Background Filter:",
  805. btnDelBg: "Reset Wallpaper", btnMenu: "Menu", btnReload: "Reload", btnTest: "Quizzes", btnFull: "Fullscreen", btnExitFull: "Exit",
  806. pnl1: "1. Basic Oscillation", pnl2: "2. Control Panel", pnl3: "3. Fourier Theorem (Sum)", pnl4: "4. Spectrogram",
  807. lblStep: "Frequency Step:", lblFreq: "Frequency (Hz):", lblAmp: "Amplitude (%):", lblPhase: "Phase Shift (°):", lblSpeed: "Wave Speed:",
  808. btnAudio: "Audio", btnMute: "Mute", btnPause: "Pause", btnPlay: "Play", btnClip: "Non-linear Dist.", btnUnclip: "Disable", btnReset: "Reset",
  809. btnSine: "Sine", btnSquare: "Square", btnTriangle: "Triangle", btnSaw: "Sawtooth", btnSave: "Save", btnSaveCompact: "Snap",
  810. eduHeader: "📚 Wave Mechanics Academy", eduCard1Title: "〰 Nature of Mechanical Waves",
  811. eduCard1Text: "A mechanical wave propagates oscillatory disturbances in an elastic medium, transferring energy without mass transfer.",
  812. eduCard1Params: "Key physical parameters:", eduParam1: "• <b>Period (T):</b> time of one full oscillation.", eduParam2: "• <b>Frequency (V):</b> number of full cycles per 1 second.", eduParam3: "• <b>Angular frequency (w):</b> angular rate of phase change.", eduParam4: "• <b>Phase shift (θ):</b> initial displacement of the phase angle.",
  813. eduCard2Title: "📉 Fourier Theorem & Analysis", eduCard2Text1: "Any complex periodic signal can be represented as an exact sum of harmonic sine waves.",
  814. eduCard2Text2: "This is the basis of all digital signal processing: from audio equalization to data packet transmission over Wi-Fi and 5G cellular networks.",
  815. calcTitle: "🧮 Wavelength Calculator", calc2Title: "⏱ Period Calculator", calcCustomMedium: "Custom...",
  816. faqTitle: "💬 Dynamic Questions and Answers (FAQ)",
  817. resTitleRU: "🇷🇺 Russian Educational Platforms",
  818. resTitleINT: "🌍 International Simulators",
  819. resPhet: "PhET: Wave on a String", resAcademo: "Academo: Wave Interference", resFalstad: "Falstad: Synth",
  820. resVirtulab: "VirtuLab: Science Labs", resInterneturok: "InternetUrok: Physics", resFoxford: "Foxford Tutorials",
  821. testHeader: "Knowledge Testing", testSelectSub: "Select test mode:", btnCheck: "Submit",
  822. modeExp: "Express", time5: "5 (No timer)", modeStd: "Standard", time15: "15 (5 min)", modeFull: "Full Course", time30: "30 (10 min)", modeUltra: "Ultra Pro", time50: "50 (15 min)", btnExit: "Exit", btnTryAgain: "Try Again",
  823. telWave: "Wave", telF: "Frequency", telA: "Amplitude", telClip: "Non-linear", telXAxis: "X Axis", clipOn: "ON", clipOff: "OFF",
  824. lblTelPhase: "Phase", telY: "Displacement", telSumY: "Σy", telPeaks: "Peaks", lblTimeLeft: "Time left",
  825. navHeader: "Sections & Info", navMain: "Dashboard", navTest: "Testing Quiz", navFormulas: "Formulas Reference", navAbout: "About HarmoniX",
  826. lblHeightResize: "Canvas Height:", modalFormulasTitle: "Formulas Reference", modalAboutTitle: "About HarmoniX Platform",
  827. modalExtLinks: "🎥 Recommended Videos:", linkRutube: "Rutube: Wave Physics Lectures", linkVK: "VK Video: Waves & Oscillations", linkYT: "YouTube: Pavel Victor Lesson", linkWiki1: "Fourier Series (Wikipedia)",
  828. aboutDescTitle: "ℹ️ Philosophy of the Project", aboutDescText: "HarmoniX is an interactive simulation suite for visual wave physics, superposition, and Fourier analysis.",
  829. aboutHistoryTitle: "📜 Changelog",
  830. form1: "<b>Traveling Wave Eq:</b><br> y(x,t) = A · cos(w·t - k·x + θ)", form2: "<b>Harmonic Oscillation:</b><br> y(t) = A · cos(w·t + θ)",
  831. form3: "<b>Period & Frequency:</b><br> T = 1 / V", form4: "<b>Angular Frequency:</b><br> w = 2 · π · V", form5: "<b>Wavelength:</b><br> λ = v / V = v · T",
  832. form6: "<b>Wave Number:</b><br> k = 2 · π / λ", form7: "<b>Doppler Effect:</b><br> f' = f · (v ± v₀) / (v ∓ vₛ)", form8: "<b>Acoustic Impedance:</b><br> Z = ρ · v",
  833. themes: { default: "⚪ Light (Default)", dark: "⚫ Dark (Night)", neon: "🟣 Neon (Cyberpunk)", ocean: "🌊 Ocean (Deep)", forest: "🌲 Forest (Natural)", sunset: "🌅 Sunset (Warm)", cyber: "🟡 Cyber (Yellow)", coffee: "☕ Latte (Coffee)", space: "🌌 Deep Space", sakura: "🌸 Sakura (Pink)" },
  834. filters: { none: "🚫 No Filter", "blur(14px) saturate(140%)": "🌫 Deep Blur", "invert(100%)": "💡 Invert / Negative", "contrast(150%) brightness(85%)": "🎬 Cinematic", "sepia(100%)": "📜 Vintage Sepia" },
  835. hz: "Hz", meters: "m", sec: "s",
  836. tziTitle: "InfoSec: Acoustic Protection",
  837. tziDesc: "Activation of white noise generator for speech masking against interception:",
  838. btnTziStart: "Activate Masking", btnTziStop: "Disable Masking",
  839. calcAirDesc: "Medium: Air (20°C)", calcSpeedLabel: "Speed (m/s):", calcCycle: "1-Cycle Duration (Period):",
  840. hintYellow: "Look for yellow [?] circles for hints!", btnPickImg: "📂 Choose Picture", btnDelBg: "Reset Wallpaper",
  841. testTimeUp: "⏰ Time is up! Answers were checked automatically.",
  842. testReviewTitle: "Detailed Questions Review:", testSkipped: "Skipped",
  843. testResTitle: "Final Result:", testResOf: "out of",
  844. testRes90: "🎉 Excellent! Expert level.", testRes60: "👍 Good, solid knowledge.", testResLow: "😅 We recommend reviewing the theory on the site.",
  845. omegaLabel: "Angular Freq (ω):", radS: "rad/s", lblFreqShort: "ν (Hz)", lblPhaseShort: "Phase",
  846. btnPrev: "Back", btnNext: "Next", qOf: "of", qWord: "Question", wikiLink: "Read more on Wikipedia", hyperPhysics: "🔗 HyperPhysics Database (GSU)",
  847. fConst: "⚛️ Fundamental constants", c1: "Speed of light (c)", c2: "Speed of sound (v)", c3: "Pi Number (π)"
  848. }
  849. };
  850. const globalFaqData = {
  851. ru: [
  852. { q: "Что такое Фигуры Лиссажу (Осциллограф X-Y)?", a: "Это <b>замкнутые траектории</b>, прочерчиваемые точкой, совершающей одновременно два гармонических колебания во <b>взаимно перпендикулярных направлениях</b>. Включите этот режим кнопкой «Лиссажу» в меню! <br><br><a href='https://ru.wikipedia.org/wiki/%D0%A4%D0%B8%D0%B3%D1%83%D1%80%D1%8B_%D0%9B%D0%B8%D1%81%D1%81%D0%B0%D0%B6%D1%83' target='_blank' class='faq-link'>Узнать подробнее (Википедия)</a>" },
  853. { q: "Как фазовый сдвиг влияет на интерференцию?", a: "При сдвиге 0° волны <b>синфазны</b> и возникает <span class='key-term'>конструктивная интерференция</span> (усиление). Если сдвиг 180° (противофаза) — волны взаимно гасятся. Этот эффект используется в системах активного шумоподавления (ANC)." },
  854. { q: "Что происходит со спектром при нелинейном ограничении (клиппинге)?", a: "При клиппинге верхушки синусоиды срезаются, приближая сигнал к прямоугольной форме (меандру). Это рождает <span class='key-term'>бесконечное число нечетных паразитных гармоник</span>, которые ярко видны в спектрограмме." },
  855. { q: "Что показывает теорема Фурье?", a: "Она доказывает, что любой сложный, но периодический сигнал можно точно собрать (синтезировать) из набора <b>простых бесконечных синусоид</b> разных частот и амплитуд." },
  856. { q: "От чего зависит скорость волны?", a: "Скорость механической волны зависит исключительно от свойств среды (ее плотности, упругости, температуры). В воздухе это ~340 м/с, в воде ~1480 м/с, в стали почти <b>6000 м/с</b>!" },
  857. { q: "Что такое Акустическая маскировка (ТЗИ)?", a: "<b>Техническая защита информации (ТЗИ)</b> в акустике включает генерацию виброакустического белого шума, маскирующего речевую информацию от подслушивания через окна, стены и вентиляцию." },
  858. { q: "Почему звуковая волна не переносит вещество?", a: "Потому что частицы среды совершают лишь малые колебательные движения возле своего положения равновесия, сталкиваясь с соседями и передавая им только энергию (импульс)." },
  859. { q: "В чем разница между продольной и поперечной волной?", a: "В поперечной волне частицы колеблются <b>перпендикулярно</b> направлению движения волны (волна на струне). В продольной — <b>вдоль</b> направления волны (звук в воздухе)." }
  860. ],
  861. en: [
  862. { q: "What are Lissajous Figures (X-Y Oscilloscope)?", a: "These are <b>closed trajectories</b> drawn by a point undergoing two harmonic oscillations in mutually perpendicular directions. Toggle it via the «Lissajous» menu button! <br><br><a href='https://en.wikipedia.org/wiki/Lissajous_curve' target='_blank' class='faq-link'>Read more (Wikipedia)</a>" },
  863. { q: "How does phase shift affect interference?", a: "At 0° shift waves are <b>in-phase</b> creating <span class='key-term'>constructive interference</span> (amplification). 180° causes complete destructive interference (cancellation, ANC technology)." },
  864. { q: "What happens to the spectrum during non-linear clipping?", a: "Clipping shears sine peaks into a square profile, yielding an <span class='key-term'>infinite train of odd harmonic spikes</span>." },
  865. { q: "What does Fourier's theorem prove?", a: "It proves any periodic wave can be synthesized from a discrete harmonic series of <b>pure sine waves</b>." },
  866. { q: "What determines wave propagation speed?", a: "Medium elasticity and density exclusively: ~340 m/s in air, ~1480 m/s in water, <b>~5900 m/s</b> in steel." },
  867. { q: "What is Acoustic Masking (InfoSec)?", a: "<b>Information Security</b> in acoustics uses active vibroacoustic white noise generators to mask human speech from optical, laser, and vibration bugs." },
  868. { q: "Why don't sound waves transport mass?", a: "Particles oscillate purely locally around equilibrium points, transferring momentum elastically." },
  869. { q: "Transverse vs. Longitudinal waves?", a: "In transverse waves, particle displacement is perpendicular to wave propagation. In longitudinal waves, it's parallel." }
  870. ]
  871. };
  872. const mediaList = [
  873. { ru: "Воздух (20°C)", en: "Air (20°C)", v: 343 }, { ru: "Воздух (0°C)", en: "Air (0°C)", v: 331 },
  874. { ru: "Гелий", en: "Helium", v: 965 }, { ru: "Водород", en: "Hydrogen", v: 1284 },
  875. { ru: "Углекислый газ", en: "Carbon Dioxide", v: 259 }, { ru: "Вода (Пресная)", en: "Fresh Water", v: 1480 },
  876. { ru: "Морская вода", en: "Sea Water", v: 1530 }, { ru: "Керосин", en: "Kerosene", v: 1324 },
  877. { ru: "Ртуть", en: "Mercury", v: 1450 }, { ru: "Глицерин", en: "Glycerol", v: 1904 },
  878. { ru: "Свинец", en: "Lead", v: 2160 }, { ru: "Золото", en: "Gold", v: 3240 },
  879. { ru: "Серебро", en: "Silver", v: 2680 }, { ru: "Лед", en: "Ice", v: 3980 },
  880. { ru: "Дерево (Сосна)", en: "Pine Wood", v: 3320 }, { ru: "Дерево (Дуб)", en: "Oak Wood", v: 4000 },
  881. { ru: "Кирпич", en: "Brick", v: 4180 }, { ru: "Бетон", en: "Concrete", v: 4250 },
  882. { ru: "Медь", en: "Copper", v: 4700 }, { ru: "Стекло (Оконное)", en: "Window Glass", v: 5640 },
  883. { ru: "Стекло (Кварцевое)", en: "Quartz Glass", v: 5370 }, { ru: "Сталь", en: "Steel", v: 5900 },
  884. { ru: "Чугун", en: "Cast Iron", v: 4480 }, { ru: "Железо", en: "Iron", v: 5950 },
  885. { ru: "Алюминий", en: "Aluminum", v: 6420 }, { ru: "Титановый сплав", en: "Titanium Alloy", v: 6070 },
  886. { ru: "Бериллий", en: "Beryllium", v: 12890 }, { ru: "Резина", en: "Rubber", v: 150 },
  887. { ru: "Пробка", en: "Cork", v: 500 }, { ru: "Алмаз", en: "Diamond", v: 12000 }
  888. ];
  889. function toggleTZI() {
  890. isMaskingTZI = !isMaskingTZI;
  891. let btn = document.getElementById('btnTziMode');
  892. if (btn) {
  893. if (isMaskingTZI) {
  894. btn.innerHTML = `<span class="emoji-fix">🔓</span> <span data-i18n="btnTziStop">${i18n[currentLang].btnTziStop}</span>`;
  895. btn.style.background = '#F44336'; btn.style.boxShadow = '0 0 15px rgba(244,67,54,0.5)';
  896. startNoise(); playAudioCue('warn');
  897. } else {
  898. btn.innerHTML = `<span class="emoji-fix">🔒</span> <span data-i18n="btnTziStart">${i18n[currentLang].btnTziStart}</span>`;
  899. btn.style.background = 'var(--tzi-color)'; btn.style.boxShadow = '0 0 15px var(--tzi-glow)';
  900. stopNoise(); playAudioCue('mainPulse');
  901. }
  902. }
  903. }
  904. function startNoise() {
  905. if (!waveCtx) initAudio(); if (!waveCtx) return;
  906. const bufferSize = waveCtx.sampleRate * 2; const buffer = waveCtx.createBuffer(1, bufferSize, waveCtx.sampleRate); const data = buffer.getChannelData(0);
  907. for (let i = 0; i < bufferSize; i++) { data[i] = Math.random() * 2 - 1; }
  908. noiseSource = waveCtx.createBufferSource(); noiseSource.buffer = buffer; noiseSource.loop = true;
  909. let noiseGain = waveCtx.createGain(); noiseGain.gain.value = 0.08;
  910. noiseSource.connect(noiseGain); noiseGain.connect(waveCtx.destination); noiseSource.start();
  911. }
  912. function stopNoise() { if (noiseSource) { try { noiseSource.stop(); noiseSource.disconnect(); } catch(e){} noiseSource = null; } }
  913. function initAudio() {
  914. if (!uiCtx) uiCtx = new (window.AudioContext || window.webkitAudioContext)();
  915. if (uiCtx.state === 'suspended') uiCtx.resume();
  916. if (!waveCtx) {
  917. waveCtx = new (window.AudioContext || window.webkitAudioContext)();
  918. for (let i = 0; i < 3; i++) {
  919. let osc = waveCtx.createOscillator(); let gain = waveCtx.createGain();
  920. osc.type = 'sine'; osc.frequency.setValueAtTime(waves[i].f, waveCtx.currentTime);
  921. gain.gain.setValueAtTime(0, waveCtx.currentTime); osc.connect(gain);
  922. gain.connect(waveCtx.destination); osc.start(); oscillators.push({ osc: osc, gain: gain });
  923. }
  924. }
  925. if (waveCtx.state === 'suspended') waveCtx.resume();
  926. }
  927. function startApp() {
  928. try { initAudio(); } catch(e){}
  929. let ws = document.getElementById('welcome-screen');
  930. let mainNav = document.getElementById('main-nav-bar');
  931. let mainApp = document.getElementById('main-app');
  932. if(ws) { ws.style.opacity = '0'; ws.style.pointerEvents = 'none'; }
  933. setTimeout(() => {
  934. if(ws) ws.style.display = 'none';
  935. if(mainNav) mainNav.style.display = 'flex';
  936. if(mainApp) mainApp.style.display = 'flex';
  937. try { buildSelectOptions(); } catch(e){}
  938. try { setLang('ru'); } catch(e){}
  939. try { setPreset('sine'); } catch(e){}
  940. setTimeout(() => {
  941. try { resizeAllCanvases(); } catch(e){}
  942. if (!animationFrameId) { draw(); }
  943. try { populateMediumSelect(currentLang); } catch(e){}
  944. try { calculateWaveLength(); } catch(e){}
  945. try { playAudioCue('glassBell'); } catch(e){}
  946. }, 50);
  947. }, 300);
  948. }
  949. function playAudioCue(type, param = 0) {
  950. try {
  951. if (!uiCtx) return;
  952. const now = uiCtx.currentTime; const osc = uiCtx.createOscillator(); const gain = uiCtx.createGain();
  953. osc.connect(gain); gain.connect(uiCtx.destination);
  954. if (type === 'glassBell') { osc.type = 'sine'; osc.frequency.setValueAtTime(1400, now); osc.frequency.exponentialRampToValueAtTime(1900, now + 0.12); gain.gain.setValueAtTime(0.06, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.18); osc.start(now); osc.stop(now + 0.18); }
  955. else if (type === 'mainPulse') { osc.type = 'triangle'; osc.frequency.setValueAtTime(260, now); osc.frequency.exponentialRampToValueAtTime(480, now + 0.09); gain.gain.setValueAtTime(0.12, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12); osc.start(now); osc.stop(now + 0.12); }
  956. else if (type === 'sliderTone') { osc.type = 'sine'; let f = 300 + (param * 10); osc.frequency.setValueAtTime(Math.min(2500, f), now); gain.gain.setValueAtTime(0.04, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.05); osc.start(now); osc.stop(now + 0.05); }
  957. else if (type === 'phaseTone') { osc.type = 'triangle'; let f = 180 + (param * 1.5); osc.frequency.setValueAtTime(f, now); gain.gain.setValueAtTime(0.05, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.06); osc.start(now); osc.stop(now + 0.06); }
  958. else if (type === 'calcKey') { osc.type = 'sawtooth'; osc.frequency.setValueAtTime(950, now); osc.frequency.exponentialRampToValueAtTime(200, now + 0.04); gain.gain.setValueAtTime(0.04, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.04); osc.start(now); osc.stop(now + 0.04); }
  959. else if (type === 'menuClick') { osc.type = 'sine'; osc.frequency.setValueAtTime(750, now); gain.gain.setValueAtTime(0.06, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08); osc.start(now); osc.stop(now + 0.08); }
  960. else if (type === 'cameraSnap') { osc.type = 'square'; osc.frequency.setValueAtTime(800, now); osc.frequency.exponentialRampToValueAtTime(100, now + 0.1); gain.gain.setValueAtTime(0.15, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1); osc.start(now); osc.stop(now + 0.1); }
  961. else if (type === 'tick') { osc.type = 'sine'; osc.frequency.setValueAtTime(1000, now); gain.gain.setValueAtTime(0.03, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.05); osc.start(now); osc.stop(now + 0.05); }
  962. else if (type === 'warn') { osc.type = 'square'; osc.frequency.setValueAtTime(400, now); osc.frequency.setValueAtTime(300, now + 0.1); gain.gain.setValueAtTime(0.05, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3); osc.start(now); osc.stop(now + 0.3); }
  963. } catch(e) {}
  964. }
  965. window.playSfx = playAudioCue;
  966. document.addEventListener('mouseover', e => {
  967. if (e.target.classList && e.target.classList.contains('hint-icon')) {
  968. const tt = document.getElementById('global-tooltip'); if(!tt) return;
  969. let hintKey = e.target.getAttribute('data-i18n-hint');
  970. if(hintKey && i18n[currentLang][hintKey]) { tt.innerHTML = i18n[currentLang][hintKey]; }
  971. else { tt.innerHTML = e.target.getAttribute('data-hint'); }
  972. tt.style.opacity = 1; const rect = e.target.getBoundingClientRect();
  973. let x = rect.left + rect.width / 2 - tt.offsetWidth / 2; let y = rect.top - tt.offsetHeight - 12;
  974. if (x < 10) x = 10; if (x + tt.offsetWidth > window.innerWidth - 10) x = window.innerWidth - tt.offsetWidth - 10;
  975. if (y < 10) y = rect.bottom + 12; tt.style.transform = `translate(${x}px, ${y}px)`; playAudioCue('hover');
  976. }
  977. }, true);
  978. document.addEventListener('mouseout', e => {
  979. if (e.target.classList && e.target.classList.contains('hint-icon')) { const tt = document.getElementById('global-tooltip'); if(tt) tt.style.opacity = 0; }
  980. }, true);
  981. function buildSelectOptions() {
  982. const curTheme = document.documentElement.getAttribute('data-theme') || 'default';
  983. const themeSel = document.getElementById('themeSelect');
  984. if (themeSel) { themeSel.innerHTML = ''; for (let k in i18n[currentLang].themes) { let opt = document.createElement('option'); opt.value = k; opt.innerHTML = i18n[currentLang].themes[k]; if (k === curTheme) opt.selected = true; themeSel.appendChild(opt); } }
  985. const filterSel = document.getElementById('bgFilter');
  986. if (filterSel) { const curFilter = document.getElementById('bg-layer').style.filter || 'none'; filterSel.innerHTML = ''; for (let f in i18n[currentLang].filters) { let opt = document.createElement('option'); opt.value = f; opt.innerHTML = i18n[currentLang].filters[f]; if (f === curFilter) opt.selected = true; filterSel.appendChild(opt); } }
  987. }
  988. function safeSetText(id, text) { let el = document.getElementById(id); if (el) el.innerText = text; }
  989. function renderDynamicFAQ(lang) {
  990. const container = document.getElementById('dynamic-faq-container'); if(!container) return; container.innerHTML = '';
  991. let pool = globalFaqData[lang] || globalFaqData['ru']; let shuffled = pool.sort(() => 0.5 - Math.random()).slice(0, 4);
  992. shuffled.forEach(item => {
  993. let btn = document.createElement('button'); btn.className = 'accordion-btn glass-element interactive-hover btn-shine'; btn.onclick = function() { toggleAccordion(this); playAudioCue('menuClick'); };
  994. btn.innerHTML = `<span>${item.q}</span> <span>▼</span>`;
  995. let content = document.createElement('div'); content.className = 'accordion-content'; content.innerHTML = `<p>${item.a}</p>`;
  996. container.appendChild(btn); container.appendChild(content);
  997. });
  998. }
  999. function populateMediumSelect(lang) {
  1000. const sel = document.getElementById('mediumSelect'); if(!sel) return;
  1001. sel.innerHTML = `<option value="custom" data-i18n="calcCustomMedium">${i18n[lang].calcCustomMedium}</option>`;
  1002. mediaList.forEach(m => { let opt = document.createElement('option'); opt.value = m.v; opt.innerText = `${m[lang] || m.ru} (${m.v})`; sel.appendChild(opt); });
  1003. let inpVal = document.getElementById('calcSpeedCustom').value; let found = Array.from(sel.options).find(o => o.value == inpVal);
  1004. sel.value = found ? found.value : 'custom';
  1005. }
  1006. function setLang(lang) {
  1007. currentLang = lang;
  1008. document.querySelectorAll('.lang-btn').forEach(btn => btn.classList.remove('active'));
  1009. let activeLangBtn = document.querySelector('.lang-' + lang); if(activeLangBtn) activeLangBtn.classList.add('active');
  1010. document.querySelectorAll('[data-i18n]').forEach(el => { const key = el.getAttribute('data-i18n'); if (i18n[lang][key]) el.innerHTML = i18n[lang][key]; });
  1011. document.querySelectorAll('.unit-freq').forEach(el => { el.innerText = i18n[lang].hz; });
  1012. document.querySelectorAll('.calc-m').forEach(el => { el.innerText = i18n[lang].meters || 'м'; });
  1013. document.querySelectorAll('.calc-s').forEach(el => { el.innerText = i18n[lang].sec || 'с'; });
  1014. let btnTZI = document.getElementById('btnTziMode');
  1015. if(btnTZI) { btnTZI.innerHTML = `<span class="emoji-fix">🔒</span> ${isMaskingTZI ? i18n[lang].btnTziStop : i18n[lang].btnTziStart}`; }
  1016. buildSelectOptions(); updateDynamicButtons(); updateTelemetryUI(); populateMediumSelect(lang); calculateWaveLength(); renderDynamicFAQ(lang);
  1017. if (typeof updateQuizStepIndicator === 'function') { updateQuizStepIndicator(); }
  1018. }
  1019. function updateDynamicButtons() {
  1020. const btnTop = document.getElementById('btnTopLabels'); const btnSide = document.getElementById('btnLabelsToggle');
  1021. const txtLab = currentLang === 'ru' ? (showLabels ? 'Подписи: Вкл' : 'Подписи: Выкл') : (showLabels ? 'Labels: On' : 'Labels: Off');
  1022. if (btnTop) btnTop.innerHTML = `<span class="emoji-fix">📊</span> ${showLabels ? (currentLang==='ru'?'Подписи':'Labels') : (currentLang==='ru'?'Без подписей':'No Labels')}`;
  1023. if (btnSide) { btnSide.innerHTML = txtLab; btnSide.classList.toggle('active', showLabels); }
  1024. const btnGlow = document.getElementById('btnGlowToggle');
  1025. if (btnGlow) { btnGlow.innerHTML = currentLang === 'ru' ? (isGlowOn ? 'Неон: Вкл' : 'Неон: Выкл') : (isGlowOn ? 'Neon: On' : 'Neon: Off'); btnGlow.classList.toggle('active', isGlowOn); }
  1026. const btnGrid = document.getElementById('btnGridToggle');
  1027. if (btnGrid) { btnGrid.innerHTML = currentLang === 'ru' ? (isGridOn ? '<span class="emoji-fix">🕸</span> Сетка: Вкл' : '<span class="emoji-fix">🕸</span> Сетка: Выкл') : (isGridOn ? '<span class="emoji-fix">🕸</span> Grid: On' : '<span class="emoji-fix">🕸</span> Grid: Off'); btnGrid.classList.toggle('active', isGridOn); }
  1028. const btnLis = document.getElementById('btnLissajous');
  1029. if (btnLis) { btnLis.innerHTML = currentLang === 'ru' ? (isLissajous ? '<span class="emoji-fix">🌀</span> Лиссажу: Вкл' : '<span class="emoji-fix">🌀</span> Лиссажу: Выкл') : (isLissajous ? '<span class="emoji-fix">🌀</span> Lissajous: On' : '<span class="emoji-fix">🌀</span> Lissajous: Off'); btnLis.classList.toggle('active', isLissajous); }
  1030. let btnAudio = document.getElementById('btnAudio');
  1031. if (btnAudio) { btnAudio.classList.toggle('active', isAudioOn); btnAudio.innerHTML = isAudioOn ? `<span class="emoji-fix">🔇</span> ${i18n[currentLang].btnMute}` : `<span class="emoji-fix">🔊</span> ${i18n[currentLang].btnAudio}`; }
  1032. let btnPause = document.getElementById('btnPause');
  1033. if (btnPause) { btnPause.classList.toggle('active', isPaused); btnPause.innerHTML = isPaused ? `<span class="emoji-fix">▶</span> ${i18n[currentLang].btnPlay}` : `<span class="emoji-fix">⏸</span> ${i18n[currentLang].btnPause}`; }
  1034. let btnClip = document.getElementById('btnClip');
  1035. if (btnClip) { btnClip.classList.toggle('active', isClipped); btnClip.querySelector('span:last-child').previousElementSibling.innerHTML = isClipped ? i18n[currentLang].btnUnclip : i18n[currentLang].btnClip; }
  1036. }
  1037. function updateSpeedValue(val) { animSpeedMultiplier = parseFloat(val) || 1.0; let el = document.getElementById('valSpeedDisp'); if(el) el.innerText = animSpeedMultiplier.toFixed(1) + "x"; playAudioCue('phaseTone', animSpeedMultiplier * 100); }
  1038. function setCanvasHeight(val) { document.querySelectorAll('.canvas-wrapper').forEach(w => { w.style.height = val + 'px'; }); resizeAllCanvases(); }
  1039. function toggleLabels() { showLabels = !showLabels; document.querySelectorAll('.chart-telemetry').forEach(el => { if(el) el.classList.toggle('hidden', !showLabels); }); const btnSide = document.getElementById('btnLabelsToggle'); if (btnSide) { btnSide.classList.toggle('active', showLabels); } updateDynamicButtons(); playAudioCue('click'); }
  1040. function toggleGlow() { isGlowOn = !isGlowOn; updateDynamicButtons(); playAudioCue('click'); }
  1041. function toggleGrid() { isGridOn = !isGridOn; updateDynamicButtons(); playAudioCue('click'); }
  1042. function toggleLissajous() { isLissajous = !isLissajous; let lm = document.getElementById('lblLisMode'); if (lm) lm.innerText = isLissajous ? "(Lissajous 2D)" : ""; updateDynamicButtons(); playAudioCue('mainPulse'); }
  1043. function setGlassState(state) { isGlassOn = state; let onBtn = document.getElementById('btnGOn'); let offBtn = document.getElementById('btnGOff'); if(onBtn && offBtn) { onBtn.classList.toggle('active', state); offBtn.classList.toggle('active', !state); } updateGlassIntensity(); playAudioCue('click'); }
  1044. function updateGlassIntensity() {
  1045. if (!isGlassOn) {
  1046. document.documentElement.style.setProperty('--glass-alpha', '0.98'); document.documentElement.style.setProperty('--glass-blur', '0px');
  1047. document.documentElement.style.setProperty('--glass-saturate', '100%'); document.documentElement.style.setProperty('--glass-border', 'rgba(204, 204, 204, 0.6)'); return;
  1048. }
  1049. let el = document.getElementById('glassIntensity'); if(!el) return;
  1050. let val = parseInt(el.value); let alpha = 0.95 - (val / 100) * 0.75; let blur = (val / 100) * 30; let saturate = 100 + (val / 100) * 100;
  1051. document.documentElement.style.setProperty('--glass-alpha', alpha.toFixed(2)); document.documentElement.style.setProperty('--glass-blur', blur + 'px');
  1052. document.documentElement.style.setProperty('--glass-saturate', saturate + '%'); document.documentElement.style.setProperty('--glass-border', 'rgba(255, 255, 255, ' + (0.15 + (val/100)*0.35) + ')');
  1053. }
  1054. function autoGlass() {
  1055. const bgLayer = document.getElementById('bg-layer'); if(!bgLayer) return;
  1056. const bgImage = bgLayer.style.backgroundImage; const filterEl = document.getElementById('bgFilter'); const filter = filterEl ? filterEl.value : 'none';
  1057. let idealValue = 30; if (bgImage && bgImage !== 'none') { idealValue = 85; if (filter.includes('blur')) idealValue = 50; }
  1058. let glassEl = document.getElementById('glassIntensity'); if(glassEl) glassEl.value = idealValue;
  1059. setGlassState(true); playAudioCue('mainPulse');
  1060. }
  1061. function resizeCanvasElement(canvas) {
  1062. if(!canvas || !canvas.parentElement) return;
  1063. const w = Math.floor(canvas.parentElement.clientWidth); const h = Math.floor(canvas.parentElement.clientHeight);
  1064. if (w === 0 || h === 0) return;
  1065. if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }
  1066. }
  1067. function resizeAllCanvases() { resizeCanvasElement(document.getElementById('canvas-single')); resizeCanvasElement(document.getElementById('canvas-sum')); resizeCanvasElement(document.getElementById('canvas-spectrum')); }
  1068. window.addEventListener('resize', resizeAllCanvases, { passive: true });
  1069. function resetCanvasSizes() {
  1070. document.querySelectorAll('.canvas-wrapper').forEach(w => { w.style.height = '260px'; });
  1071. let el = document.getElementById('canvasHeightSlider'); if(el) el.value = 260;
  1072. resizeAllCanvases(); playAudioCue('mainPulse');
  1073. }
  1074. function updateColorsCache() { cachedBaseColor = getComputedStyle(document.documentElement).getPropertyValue('--base-wave').trim() || '#2196F3'; }
  1075. function toggleFullscreen() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen().catch(err=>{}); } else { if (document.exitFullscreen) document.exitFullscreen(); } }
  1076. document.addEventListener('fullscreenchange', () => { updateDynamicButtons(); });
  1077. function toggleSidebar() { const sidebar = document.getElementById('sidebar'); const overlay = document.getElementById('sidebar-overlay'); if(!sidebar || !overlay) return; if (sidebar.classList.contains('open')) { sidebar.classList.remove('open'); overlay.style.display = 'none'; } else { sidebar.classList.add('open'); overlay.style.display = 'block'; } }
  1078. function showInfoModal(id) { let ov = document.getElementById('info-overlay'); if(!ov) return; ov.style.display = 'flex'; setTimeout(()=> ov.classList.add('show'), 10); document.querySelectorAll('.info-content').forEach(el => el.style.display = 'none'); let target = document.getElementById('modal-' + id); if(target) { target.style.display = 'block'; let delays = 0.05; target.querySelectorAll('.pop-animation').forEach(el => { el.style.animationDelay = delays + 's'; delays += 0.05; el.classList.remove('pop-animation'); void el.offsetWidth; el.classList.add('pop-animation'); }); } playAudioCue('action'); }
  1079. function closeInfoModal() { let ov = document.getElementById('info-overlay'); if(!ov) return; ov.classList.remove('show'); setTimeout(()=> { ov.style.display = 'none'; }, 300); playAudioCue('click'); }
  1080. function toggleAccordion(btn) { btn.classList.toggle("active"); var content = btn.nextElementSibling; if(!content) return; if (content.style.maxHeight) { content.style.maxHeight = null; } else { content.style.maxHeight = content.scrollHeight + "px"; } playAudioCue('click'); }
  1081. function onSelectMedium() { const sel = document.getElementById('mediumSelect'); const inp = document.getElementById('calcSpeedCustom'); if (sel.value !== 'custom') { inp.value = sel.value; } }
  1082. function onManualSpeedInput() { const sel = document.getElementById('mediumSelect'); const inp = document.getElementById('calcSpeedCustom'); let found = Array.from(sel.options).find(o => o.value == inp.value && o.value !== 'custom'); if (found) { sel.value = found.value; } else { sel.value = 'custom'; } }
  1083. function calculateWaveLength() {
  1084. let freqEl1 = document.getElementById('calcFreq1'); if(!freqEl1) return;
  1085. let freq = parseFloat(freqEl1.value); if (isNaN(freq) || freq <= 0) freq = 1;
  1086. let speedEl = document.getElementById('calcSpeedCustom'); let speedCustom = speedEl ? parseFloat(speedEl.value) || 343 : 343;
  1087. safeSetText('calcResAir', `${(speedCustom / freq).toFixed(2)}`);
  1088. let T = (1 / freq).toFixed(5); safeSetText('calcResPeriod', T);
  1089. let omega = (2 * Math.PI * freq).toFixed(1); safeSetText('calcResOmega', `${i18n[currentLang].omegaLabel} ~${omega} ${i18n[currentLang].radS}`);
  1090. const sel = document.getElementById('mediumSelect'); let descLabel = document.getElementById('lblCalcAirDesc');
  1091. if (descLabel) {
  1092. if (sel && sel.value !== 'custom' && sel.options[sel.selectedIndex]) {
  1093. let text = sel.options[sel.selectedIndex].innerText; descLabel.innerText = currentLang === 'ru' ? `Среда: ${text}` : `Medium: ${text}`;
  1094. } else {
  1095. descLabel.innerText = currentLang === 'ru' ? `Скорость ${speedCustom} м/с:` : `Speed ${speedCustom} m/s:`;
  1096. }
  1097. }
  1098. }
  1099. function updateStep(val) { freqStep = val; let elF = document.getElementById('freq'); if(elF) elF.step = val; let st10 = document.getElementById('st10'); let st100 = document.getElementById('st100'); if(st10) st10.classList.remove('active'); if(st100) st100.classList.remove('active'); let cur = document.getElementById('st' + val); if(cur) cur.classList.add('active'); }
  1100. function updateTelemetryUI() { const w = waves[currentWave]; const waveNames = ['V1', 'V2', 'V3']; const L = i18n[currentLang]; safeSetText('lblTelWave', L.telWave); safeSetText('lblTelF', L.telF); safeSetText('lblTelA', L.telA); safeSetText('lblTelClip', L.telClip); safeSetText('lblTelXAxis', L.telXAxis); safeSetText('lblTelPhase', L.lblPhaseShort); let ty = document.getElementById('telY'); if(ty && ty.previousElementSibling) ty.previousElementSibling.innerText = L.telY + ":"; let ts = document.getElementById('telSumY'); if(ts && ts.previousElementSibling) ts.previousElementSibling.innerText = L.telSumY + ":"; let tp = document.getElementById('telSpecPeaks'); if(tp && tp.previousElementSibling) tp.previousElementSibling.innerText = L.telPeaks + ":"; safeSetText('telWaveName', waveNames[currentWave]); safeSetText('telFreq', w.f); safeSetText('telAmp', w.a); safeSetText('telPhase', `${w.p}°`); safeSetText('telClipStatus', isClipped ? L.clipOn : L.clipOff); }
  1101. function updateLabels() { let w = waves[currentWave]; let fEl = document.getElementById('freq'); if(fEl) fEl.value = w.f; let fInp = document.getElementById('inputV'); if(fInp) fInp.value = w.f; let aEl = document.getElementById('amp'); if(aEl) aEl.value = w.a; let aInp = document.getElementById('inputA'); if(aInp) aInp.value = w.a; let pEl = document.getElementById('phase'); if(pEl) pEl.value = w.p; let pInp = document.getElementById('inputP'); if(pInp) pInp.value = w.p; updateTelemetryUI(); calculateWaveLength(); }
  1102. function updateWaveFromSlider(type) {
  1103. if (type === 'freq') { let el = document.getElementById('freq'); if(el) { let val = parseInt(el.value); let inEl = document.getElementById('inputV'); if(inEl) inEl.value = val; waves[currentWave].f = val; playAudioCue('sliderTone', val / 100); } }
  1104. else if (type === 'amp') { let el = document.getElementById('amp'); if(el) { let val = parseInt(el.value); let inEl = document.getElementById('inputA'); if(inEl) inEl.value = val; waves[currentWave].a = val; playAudioCue('sliderTone', val); } }
  1105. else if (type === 'phase') { let el = document.getElementById('phase'); if(el) { let val = parseInt(el.value); let inEl = document.getElementById('inputP'); if(inEl) inEl.value = val; waves[currentWave].p = val; playAudioCue('phaseTone', val); } }
  1106. updateTelemetryUI(); updateWaveAudio();
  1107. }
  1108. function updateWaveFromInput(type) {
  1109. if (type === 'freq') { let el = document.getElementById('inputV'); if(el) { let val = parseInt(el.value); if(!isNaN(val) && val >= 0 && val <= 20000) { let sEl=document.getElementById('freq'); if(sEl) sEl.value = val; waves[currentWave].f = val; } } }
  1110. else if (type === 'amp') { let el = document.getElementById('inputA'); if(el) { let val = parseInt(el.value); if(!isNaN(val) && val >= 0 && val <= 100) { let sEl=document.getElementById('amp'); if(sEl) sEl.value = val; waves[currentWave].a = val; } } }
  1111. else if (type === 'phase') { let el = document.getElementById('inputP'); if(el) { let val = parseInt(el.value); if(!isNaN(val) && val >= 0 && val <= 360) { let sEl=document.getElementById('phase'); if(sEl) sEl.value = val; waves[currentWave].p = val; } } }
  1112. playAudioCue('calcKey'); updateTelemetryUI(); updateWaveAudio();
  1113. }
  1114. function switchTab(index) { currentWave = index; document.querySelectorAll('.tabs .tab').forEach((el, i) => { if (i===index) el.classList.add('active'); else el.classList.remove('active'); }); updateLabels(); playAudioCue('click'); }
  1115. function togglePause() { isPaused = !isPaused; updateDynamicButtons(); playAudioCue('click'); }
  1116. function setPreset(type) {
  1117. currentPreset = type; document.querySelectorAll('.presets-row button').forEach(b => b.classList.remove('active')); let activeBtn = document.getElementById('pr_' + type); if(activeBtn) activeBtn.classList.add('active');
  1118. if (type === 'sine') { waves[0] = { f: 440, a: 80, p: 0 }; waves[1] = { f: 880, a: 0, p: 0 }; waves[2] = { f: 1320, a: 0, p: 0 }; isClipped = false; }
  1119. else if (type === 'square') { waves[0] = { f: 400, a: 100, p: 0 }; waves[1] = { f: 1200, a: 33, p: 0 }; waves[2] = { f: 2000, a: 20, p: 0 }; }
  1120. else if (type === 'triangle') { waves[0] = { f: 400, a: 100, p: 0 }; waves[1] = { f: 1200, a: 11, p: 180 }; waves[2] = { f: 2000, a: 4, p: 0 }; }
  1121. else if (type === 'sawtooth') { waves[0] = { f: 400, a: 100, p: 0 }; waves[1] = { f: 800, a: 50, p: 180 }; waves[2] = { f: 1200, a: 33, p: 0 }; }
  1122. updateLabels(); updateDynamicButtons(); updateWaveAudio(); playAudioCue('mainPulse');
  1123. }
  1124. function toggleClip() { isClipped = !isClipped; updateLabels(); updateDynamicButtons(); updateWaveAudio(); playAudioCue('mainPulse'); }
  1125. function resetAll() {
  1126. waves = [{ f: 440, a: 80, p: 0 }, { f: 880, a: 0, p: 0 }, { f: 1320, a: 0, p: 0 }]; isClipped = false; isLissajous = false; clearBackground();
  1127. let lisBtn = document.getElementById('btnLissajous'); if(lisBtn) { lisBtn.classList.remove('active'); }
  1128. safeSetText('lblLisMode', ""); animSpeedMultiplier = 1.0;
  1129. let elSpeed = document.getElementById('speed'); if(elSpeed) elSpeed.value = 1.0; updateSpeedValue(1.0);
  1130. setPreset('sine'); if (isPaused) togglePause(); resetCanvasSizes(); switchTab(0); updateWaveAudio(); updateDynamicButtons(); playAudioCue('error');
  1131. }
  1132. function updateWaveAudio() {
  1133. if (!waveCtx || oscillators.length === 0) return; const now = waveCtx.currentTime;
  1134. for (let i = 0; i < 3; i++) {
  1135. oscillators[i].osc.frequency.setTargetAtTime(waves[i].f, now, 0.03); oscillators[i].osc.type = isClipped ? 'square' : 'sine';
  1136. let targetVol = 0; if (isAudioOn && waves[i].a > 0) { targetVol = (waves[i].a / 100) * 0.12; if (isClipped) targetVol *= 1.4; }
  1137. oscillators[i].gain.gain.setTargetAtTime(targetVol, now, 0.05);
  1138. }
  1139. }
  1140. function toggleAudio() { try { if (waveCtx && waveCtx.state === 'suspended') waveCtx.resume(); } catch(e){} isAudioOn = !isAudioOn; updateDynamicButtons(); updateWaveAudio(); playAudioCue('action'); }
  1141. function updateColorsCache() { cachedBaseColor = getComputedStyle(document.documentElement).getPropertyValue('--base-wave').trim() || '#2196F3'; }
  1142. function changeTheme(t) { document.documentElement.setAttribute('data-theme', t); buildSelectOptions(); updateColorsCache(); playAudioCue('action'); }
  1143. function changeFilter(f) { let bl = document.getElementById('bg-layer'); if(bl) bl.style.filter = f; buildSelectOptions(); playAudioCue('click'); }
  1144. function clearBackground() { let bl = document.getElementById('bg-layer'); if(bl) bl.style.backgroundImage = 'none'; let bu = document.getElementById('bgUpload'); if(bu) bu.value = ''; playAudioCue('mainPulse'); }
  1145. function uploadBackground(e) { if (e.target.files[0]) { const r = new FileReader(); r.onload = ev => { let bl = document.getElementById('bg-layer'); if(bl) bl.style.backgroundImage = `url('${ev.target.result}')`; playAudioCue('glassBell'); }; r.readAsDataURL(e.target.files[0]); } }
  1146. function downloadGraph(id) { const c = document.getElementById(id); if(!c) return; const l = document.createElement('a'); l.download = 'HarmoniX_Graph.png'; const tc = document.createElement('canvas'); tc.width = c.width; tc.height = c.height; const tCtx = tc.getContext('2d'); tCtx.fillStyle = '#ffffff'; tCtx.fillRect(0,0,tc.width,tc.height); tCtx.drawImage(c,0,0); l.href = tc.toDataURL(); l.click(); playAudioCue('cameraSnap'); }
  1147. let lastTelemUpdate = 0;
  1148. function draw() {
  1149. if (!isPaused) time += (BASE_ANIM_SPEED * animSpeedMultiplier);
  1150. let c1 = document.getElementById('canvas-single'); let c2 = document.getElementById('canvas-sum'); let c3 = document.getElementById('canvas-spectrum');
  1151. if(c1) resizeCanvasElement(c1); if(c2) resizeCanvasElement(c2); if(c3) resizeCanvasElement(c3);
  1152. let ctx1 = c1 ? c1.getContext('2d') : null; if(ctx1) ctx1.clearRect(0, 0, c1.width, c1.height);
  1153. let ctx2 = c2 ? c2.getContext('2d') : null; if(ctx2) ctx2.clearRect(0, 0, c2.width, c2.height);
  1154. let ctx3 = c3 ? c3.getContext('2d') : null; if(ctx3) ctx3.clearRect(0, 0, c3.width, c3.height);
  1155. let baseColor = cachedBaseColor; let sumColor = "#E91E63"; let gridColor = "rgba(128,128,128,0.22)";
  1156. const wParams = waves.map(w => ({ vf: (w.f / 200) || 1, pr: w.p * (Math.PI / 180), amp: (w.a / 100) }));
  1157. const c1mid = c1 ? c1.height / 2 : 0; const c1amp = c1 ? c1.height / 2.5 : 0;
  1158. const c2mid = c2 ? c2.height / 2 : 0; const c2amp = c2 ? c2.height / 2.5 : 0;
  1159. if (isGridOn) {
  1160. if(ctx1) { ctx1.beginPath(); ctx1.strokeStyle = gridColor; ctx1.moveTo(0, c1mid); ctx1.lineTo(c1.width, c1mid); ctx1.stroke(); }
  1161. if(ctx2) { ctx2.beginPath(); ctx2.strokeStyle = gridColor; ctx2.moveTo(0, c2mid); ctx2.lineTo(c2.width, c2mid); ctx2.stroke(); }
  1162. if(ctx3) { ctx3.beginPath(); ctx3.strokeStyle = gridColor; ctx3.moveTo(0, c3.height - 18); ctx3.lineTo(c3.width, c3.height - 18); ctx3.stroke(); }
  1163. }
  1164. if (isGlowOn) {
  1165. if(ctx1) { ctx1.shadowBlur = 10; ctx1.shadowColor = baseColor; }
  1166. if(ctx2) { ctx2.shadowBlur = 10; ctx2.shadowColor = sumColor; }
  1167. if(ctx3) { ctx3.shadowBlur = 10; ctx3.shadowColor = "#4CAF50"; }
  1168. } else {
  1169. if(ctx1) ctx1.shadowBlur = 0; if(ctx2) ctx2.shadowBlur = 0; if(ctx3) ctx3.shadowBlur = 0;
  1170. }
  1171. let singleInstantY = 0;
  1172. if(ctx1 && c1.width > 0) {
  1173. ctx1.beginPath(); ctx1.strokeStyle = baseColor; ctx1.lineWidth = 2.5;
  1174. if (isLissajous) {
  1175. ctx1.moveTo(c1.width/2 + Math.cos(-time + wParams[0].pr) * wParams[0].amp * (c1.width/3), c1mid - Math.sin(-time + wParams[1].pr) * wParams[1].amp * c1amp);
  1176. for (let t = 0; t < Math.PI * 4; t += 0.05) { let lx = c1.width/2 + Math.cos(t * wParams[0].vf - time + wParams[0].pr) * wParams[0].amp * (c1.width/3); let ly = c1mid - Math.sin(t * wParams[1].vf - time + wParams[1].pr) * wParams[1].amp * c1amp; ctx1.lineTo(lx, ly); }
  1177. } else {
  1178. for (let x = 0; x < c1.width; x += 2) {
  1179. let y = Math.cos(x * 0.02 * wParams[currentWave].vf - time + wParams[currentWave].pr) * wParams[currentWave].amp * c1amp;
  1180. if (x === 0) { ctx1.moveTo(x, c1mid - y); singleInstantY = y / c1amp; } else ctx1.lineTo(x, c1mid - y);
  1181. }
  1182. }
  1183. ctx1.stroke();
  1184. }
  1185. let sumInstantY = 0;
  1186. if(ctx2 && c2.width > 0) {
  1187. let clipLimit = c2.height / 4; ctx2.beginPath();
  1188. ctx2.strokeStyle = isMaskingTZI ? '#00E676' : sumColor; ctx2.lineWidth = isMaskingTZI ? 1.5 : 2.5;
  1189. for (let x = 0; x < c2.width; x += 2) {
  1190. let sumY = 0; for (let i = 0; i < 3; i++) { sumY += Math.cos(x * 0.02 * wParams[i].vf - time + wParams[i].pr) * wParams[i].amp * c2amp; }
  1191. if (isClipped) { if (sumY > clipLimit) sumY = clipLimit; if (sumY < -clipLimit) sumY = -clipLimit; }
  1192. if (isMaskingTZI) { sumY += (Math.random() * 40 - 20); }
  1193. if (x === 0) { ctx2.moveTo(x, c2mid - sumY/1.5); sumInstantY = sumY / c2amp; } else ctx2.lineTo(x, c2mid - sumY/1.5);
  1194. }
  1195. ctx2.stroke();
  1196. }
  1197. let nowTime = performance.now();
  1198. if (showLabels && (nowTime - lastTelemUpdate > 90)) {
  1199. lastTelemUpdate = nowTime;
  1200. let tY = document.getElementById('telY'); if(tY) tY.innerText = isLissajous ? "2D" : (singleInstantY >= 0 ? "+" : "") + singleInstantY.toFixed(2);
  1201. let tSY = document.getElementById('telSumY'); if(tSY) tSY.innerText = (sumInstantY >= 0 ? "+" : "") + sumInstantY.toFixed(2);
  1202. let activePeaks = waves.filter(item => item.a > 0).length; if (isClipped && waves[0].a > 0) activePeaks += 4;
  1203. let tSP = document.getElementById('telSpecPeaks'); if(tSP) tSP.innerText = activePeaks;
  1204. }
  1205. if(ctx3 && c3.width > 0) {
  1206. ctx3.shadowBlur = 0;
  1207. if (showLabels) { ctx3.fillStyle = "rgba(128,128,128,0.85)"; ctx3.font = "10px Arial"; ctx3.fillText("5k", (5000 / 20000) * c3.width, c3.height - 3); ctx3.fillText("10k", (10000 / 20000) * c3.width, c3.height - 3); ctx3.fillText("15k", (15000 / 20000) * c3.width, c3.height - 3); }
  1208. ctx3.fillStyle = "#4CAF50";
  1209. if (isGlowOn) { ctx3.shadowBlur = 10; ctx3.shadowColor = "#4CAF50"; }
  1210. for (let i = 0; i < 3; i++) { if (wParams[i].amp > 0) { let xPos = (waves[i].f / 20000) * c3.width; let h = wParams[i].amp * (c3.height - 32); ctx3.fillRect(Math.max(xPos, 5), c3.height - 18 - h, 10, h); } }
  1211. if (isClipped && wParams[0].amp > 0) {
  1212. ctx3.fillStyle = "rgba(255, 87, 34, 0.9)";
  1213. if (isGlowOn) { ctx3.shadowBlur = 10; ctx3.shadowColor = "rgba(255, 87, 34, 1)"; }
  1214. for (let i = 3; i <= 9; i += 2) { let fakeF = waves[0].f * i; if (fakeF > 20000) break; let xPos = (fakeF / 20000) * c3.width; let h = (waves[0].a / (i*2) / 100) * (c3.height - 32); ctx3.fillRect(Math.max(xPos, 5), c3.height - 18 - h, 6, h); }
  1215. }
  1216. if (isMaskingTZI) {
  1217. ctx3.fillStyle = "rgba(0, 230, 118, 0.4)";
  1218. for(let i=0; i< c3.width; i+=8) { let noiseH = Math.random() * (c3.height / 3); ctx3.fillRect(i, c3.height - 18 - noiseH, 4, noiseH); }
  1219. }
  1220. ctx3.shadowBlur = 0;
  1221. }
  1222. animationFrameId = requestAnimationFrame(draw);
  1223. }
  1224. /*
  1225. * ГЕНЕРАТОР ВОПРОСОВ (С ВИКИ-ССЫЛКАМИ)
  1226. */
  1227. function generateDynamicQuestion(index) {
  1228. const freqs = [10, 20, 50, 100, 200, 440, 500, 1000]; const f = freqs[Math.floor(Math.random() * freqs.length)]; const v = [340, 1480, 5900][Math.floor(Math.random() * 3)]; const isRu = currentLang === 'ru';
  1229. const templates = [
  1230. () => { let T = (1/f).toFixed(4).replace(/\.?0+$/, ""); return { q: isRu ? `Чему равен период (T) при частоте ${f} Гц?` : `What is the period (T) at frequency ${f} Hz?`, opts: [{t: `${T} ${isRu?'с':'s'}`, c:true}, {t: `${(2/f).toFixed(4)} ${isRu?'с':'s'}`, c:false}, {t: `${(1/(f*2)).toFixed(4)} ${isRu?'с':'s'}`, c:false}], reason: isRu ? `T = 1 / V = 1 / ${f} = ${T} с.` : `T = 1 / V = 1 / ${f} = ${T} s.`, link: isRu ? 'https://ru.wikipedia.org/wiki/Период_колебаний' : 'https://en.wikipedia.org/wiki/Frequency' } },
  1231. () => { let l = (v/f).toFixed(2); return { q: isRu ? `Скорость звука ${v} м/с. Какова длина волны при ${f} Гц?` : `Sound speed is ${v} m/s. What is the wavelength at ${f} Hz?`, opts: [{t: `${l} ${isRu?'м':'m'}`, c:true}, {t: `${(v*f).toFixed(0)} ${isRu?'м':'m'}`, c:false}, {t: `${(v/(f*2)).toFixed(2)} ${isRu?'м':'m'}`, c:false}], reason: isRu ? `λ = v / V = ${v} / ${f} = ${l} м.` : `λ = v / V = ${v} / ${f} = ${l} m.`, link: isRu ? 'https://ru.wikipedia.org/wiki/Длина_волны' : 'https://en.wikipedia.org/wiki/Wavelength' } },
  1232. () => { let w = (2 * Math.PI * f).toFixed(1); return { q: isRu ? `Циклическая частота (w) при ${f} Гц?` : `Angular frequency (w) at ${f} Hz?`, opts: [{t: `≈ ${w} ${isRu?'рад/с':'rad/s'}`, c:true}, {t: `≈ ${(Math.PI * f).toFixed(1)} ${isRu?'рад/с':'rad/s'}`, c:false}, {t: `≈ ${(f / (2*Math.PI)).toFixed(1)} ${isRu?'рад/с':'rad/s'}`, c:false}], reason: isRu ? `w = 2πV = 2 * 3.14 * ${f} ≈ ${w}` : `w = 2πV = 2 * 3.14 * ${f} ≈ ${w}`, link: isRu ? 'https://ru.wikipedia.org/wiki/Круговая_частота' : 'https://en.wikipedia.org/wiki/Angular_frequency' } },
  1233. () => { return isRu ? { q: "Что утверждает теорема Фурье?", opts: [{t:"Любой периодический сигнал — это сумма синусоид", c:true}, {t:"Частота обратно пропорциональна амплитуде", c:false}, {t:"Энергия всегда константа", c:false}], reason: "Фурье доказал, что любую периодическую волну можно собрать из простых синусов.", link: 'https://ru.wikipedia.org/wiki/Ряд_Фурье' } : { q: "What does Fourier's theorem state?", opts: [{t:"Any periodic signal is a sum of sine waves", c:true}, {t:"Frequency is inversely proportional to amplitude", c:false}, {t:"Energy is always constant", c:false}], reason: "Fourier proved any periodic wave can be built from simple sines.", link: 'https://en.wikipedia.org/wiki/Fourier_series' } },
  1234. () => { return isRu ? { q: "Какая разность фаз вызывает полное гашение волн?", opts: [{t:"180°", c:true}, {t:"0°", c:false}, {t:"90°", c:false}], reason: "Сдвиг 180° (противофаза) означает, что гребень совпадает со впадиной.", link: 'https://ru.wikipedia.org/wiki/Интерференция_волн' } : { q: "What phase difference causes complete destructive interference?", opts: [{t:"180°", c:true}, {t:"0°", c:false}, {t:"90°", c:false}], reason: "A 180° shift means a crest perfectly overlaps a trough, canceling the wave.", link: 'https://en.wikipedia.org/wiki/Wave_interference' } },
  1235. () => { return isRu ? { q: "К чему приводит клиппинг (ограничение амплитуды)?", opts: [{t:"К появлению нелинейных гармоник", c:true}, {t:"К сдвигу частоты вниз", c:false}, {t:"К плавному затуханию", c:false}], reason: "Обрезанная синусоида становится похожа на меандр, который содержит множество обертонов.", link: 'https://ru.wikipedia.org/wiki/Клиппинг_(аудио)' } : { q: "What is the result of amplitude clipping?", opts: [{t:"Generation of non-linear harmonics", c:true}, {t:"Frequency downshift", c:false}, {t:"Smooth fading", c:false}], reason: "A clipped sine wave resembles a square wave, which contains many overtones.", link: 'https://en.wikipedia.org/wiki/Clipping_(audio)' } },
  1236. () => { return isRu ? { q: "Как получить фигуры Лиссажу?", opts: [{t:"Подать сигналы на оси X и Y", c:true}, {t:"Увеличить амплитуду", c:false}, {t:"Применить спектральный фильтр", c:false}], reason: "Фигуры Лиссажу строятся при сложении двух взаимно перпендикулярных колебаний.", link: 'https://ru.wikipedia.org/wiki/Фигуры_Лиссажу' } : { q: "How to generate Lissajous figures?", opts: [{t:"Apply signals to X and Y axes", c:true}, {t:"Increase amplitude to max", c:false}, {t:"Apply a spectral filter", c:false}], reason: "Lissajous figures are drawn by combining two mutually perpendicular oscillations.", link: 'https://en.wikipedia.org/wiki/Lissajous_curve' } },
  1237. () => { return isRu ? { q: "Что определяет высоту тона в звуковой волне?", opts: [{t:"Частота (V)", c:true}, {t:"Амплитуда (A)", c:false}, {t:"Фаза (θ)", c:false}], reason: "Чем выше частота колебаний, тем выше воспринимаемый тон звука.", link: 'https://ru.wikipedia.org/wiki/Высота_звука' } : { q: "What determines the pitch of a sound wave?", opts: [{t:"Frequency (V)", c:true}, {t:"Amplitude (A)", c:false}, {t:"Phase (θ)", c:false}], reason: "Higher frequency corresponds to a higher perceived pitch.", link: 'https://en.wikipedia.org/wiki/Pitch_(music)' } }
  1238. ];
  1239. return templates[index % templates.length]();
  1240. }
  1241. function shuffle(a) { return a.sort(() => Math.random() - 0.5); }
  1242. let quizAnswers = {}; let quizQuestionsList = []; let currentQuestionIdx = 0;
  1243. let quizTimerInterval = null; let quizTimeLeft = 0; let quizTotalTime = 0;
  1244. function startQuizTimer(seconds) {
  1245. clearInterval(quizTimerInterval);
  1246. const bar = document.getElementById('quizTimerBar'); const val = document.getElementById('quizTimerVal'); const prog = document.getElementById('quizTimerProgress');
  1247. if(!bar || !val) return;
  1248. val.className = 'timer-display';
  1249. if(prog) { prog.style.width = '100%'; prog.style.background = 'linear-gradient(90deg, #00E676 0%, #00B0FF 100%)'; prog.style.boxShadow = '0 0 12px rgba(0, 230, 118, 0.6)'; }
  1250. if (seconds <= 0) { bar.style.display = 'none'; return; }
  1251. bar.style.display = 'flex'; quizTimeLeft = seconds; quizTotalTime = seconds;
  1252. function updateTimerDisplay() {
  1253. let m = Math.floor(quizTimeLeft / 60); let s = quizTimeLeft % 60;
  1254. val.innerText = `${m < 10 ? '0' : ''}${m}:${s < 10 ? '0' : ''}${s}`;
  1255. if(prog) {
  1256. let pct = (quizTimeLeft / quizTotalTime) * 100; prog.style.width = `${pct}%`;
  1257. if(pct > 50) { prog.style.background = 'linear-gradient(90deg, #00E676 0%, #00B0FF 100%)'; prog.style.boxShadow = '0 0 12px rgba(0, 230, 118, 0.6)'; }
  1258. else if(pct <= 50 && pct > 20) { prog.style.background = 'linear-gradient(90deg, #FFEA00 0%, #FF9800 100%)'; prog.style.boxShadow = '0 0 12px rgba(255, 152, 0, 0.6)'; }
  1259. else { prog.style.background = 'linear-gradient(90deg, #FF5722 0%, #F44336 100%)'; prog.style.boxShadow = '0 0 12px rgba(244, 67, 54, 0.8)'; }
  1260. }
  1261. if (quizTimeLeft <= 10) { val.className = 'timer-display timer-red-blink'; playAudioCue('tick'); }
  1262. else if (quizTimeLeft <= 30) { val.className = 'timer-display timer-orange'; }
  1263. else if (quizTimeLeft === 60) { playAudioCue('warn'); val.className = 'timer-display timer-orange'; }
  1264. else { val.className = 'timer-display'; }
  1265. }
  1266. updateTimerDisplay();
  1267. quizTimerInterval = setInterval(() => {
  1268. quizTimeLeft--;
  1269. if (quizTimeLeft <= 0) { clearInterval(quizTimerInterval); checkTest(); alert(i18n[currentLang].testTimeUp); }
  1270. else { updateTimerDisplay(); }
  1271. }, 1000);
  1272. }
  1273. function openTestMenu(){
  1274. clearInterval(quizTimerInterval); let ov = document.getElementById('test-overlay'); if(!ov) return;
  1275. ov.style.display = 'flex'; setTimeout(()=> ov.classList.add('show'), 10);
  1276. const startMenu = document.getElementById('test-start-menu'); if (startMenu) startMenu.style.display = 'block';
  1277. const modesBox = document.getElementById('testModesContainer'); if (modesBox) modesBox.style.display = 'grid';
  1278. document.getElementById('quiz-process').style.display = 'none';
  1279. document.getElementById('test-result-screen').style.display = 'none';
  1280. document.getElementById('quiz-card-wrapper').style.display = 'block';
  1281. document.getElementById('quiz-nav-controls').style.display = 'flex';
  1282. let mc = document.querySelector('#test-overlay .modal-content'); if(mc) mc.classList.add('pop-animation');
  1283. playAudioCue('menuClick');
  1284. }
  1285. function startQuiz(n){
  1286. document.getElementById('test-start-menu').style.display = 'none';
  1287. document.getElementById('quiz-process').style.display = 'block';
  1288. document.getElementById('test-result-screen').style.display = 'none';
  1289. document.getElementById('quiz-card-wrapper').style.display = 'block';
  1290. document.getElementById('quiz-nav-controls').style.display = 'flex';
  1291. quizAnswers = {}; quizQuestionsList = []; currentQuestionIdx = 0;
  1292. for (let i = 0; i < n; i++) {
  1293. const item = generateDynamicQuestion(i); item.opts = shuffle([...item.opts]); quizQuestionsList.push(item);
  1294. }
  1295. renderCurrentQuestion();
  1296. if (n === 5) startQuizTimer(0); else if (n === 15) startQuizTimer(300); else if (n === 30) startQuizTimer(600); else if (n === 50) startQuizTimer(900);
  1297. playAudioCue('glassBell');
  1298. }
  1299. function renderCurrentQuestion() {
  1300. const container = document.getElementById('quiz-container'); if(!container) return; container.innerHTML = '';
  1301. const item = quizQuestionsList[currentQuestionIdx]; const total = quizQuestionsList.length;
  1302. let card = document.createElement('div'); card.className = 'question glass-element pop-animation';
  1303. let meta = document.createElement('div'); meta.className = 'question-meta';
  1304. meta.innerHTML = `<span>${i18n[currentLang].qWord} ${currentQuestionIdx + 1} ${i18n[currentLang].qOf} ${total}</span>`;
  1305. card.appendChild(meta);
  1306. let qText = document.createElement('p'); qText.innerText = item.q; card.appendChild(qText);
  1307. item.opts.forEach((opt, optIdx) => {
  1308. let lbl = document.createElement('label'); lbl.className = 'quiz-option glass-element smooth-transition';
  1309. if (quizAnswers[currentQuestionIdx] !== undefined && quizAnswers[currentQuestionIdx] === optIdx) { lbl.classList.add('selected'); }
  1310. lbl.onclick = function() {
  1311. quizAnswers[currentQuestionIdx] = optIdx;
  1312. card.querySelectorAll('.quiz-option').forEach(el => el.classList.remove('selected'));
  1313. lbl.classList.add('selected'); playAudioCue('calcKey');
  1314. };
  1315. lbl.innerHTML = `<input type="radio" name="q_opt" ${quizAnswers[currentQuestionIdx] === optIdx ? 'checked' : ''}><div class="custom-radio"></div><span class="option-text">${opt.t}</span>`;
  1316. card.appendChild(lbl);
  1317. });
  1318. container.appendChild(card); updateQuizStepIndicator();
  1319. }
  1320. function updateQuizStepIndicator() {
  1321. const total = quizQuestionsList.length;
  1322. const ind = document.getElementById('quiz-step-indicator'); if(ind) ind.innerText = `${currentQuestionIdx + 1} / ${total}`;
  1323. const btnPrev = document.getElementById('btnPrevQ'); if(btnPrev) btnPrev.disabled = (currentQuestionIdx === 0);
  1324. const btnNext = document.getElementById('btnNextQ');
  1325. if(btnNext) {
  1326. if (currentQuestionIdx === total - 1) {
  1327. btnNext.innerHTML = `<span>${i18n[currentLang].btnCheck}</span> ✅`;
  1328. btnNext.onclick = function() { checkTest(); playAudioCue('glassBell'); };
  1329. } else {
  1330. btnNext.innerHTML = `<span>${i18n[currentLang].btnNext}</span> ▶`;
  1331. btnNext.onclick = function() { nextQuestion(); playAudioCue('mainPulse'); };
  1332. }
  1333. }
  1334. }
  1335. function nextQuestion() { if (currentQuestionIdx < quizQuestionsList.length - 1) { currentQuestionIdx++; renderCurrentQuestion(); } }
  1336. function prevQuestion() { if (currentQuestionIdx > 0) { currentQuestionIdx--; renderCurrentQuestion(); } }
  1337. function closeTest(){
  1338. clearInterval(quizTimerInterval); let ov = document.getElementById('test-overlay'); if(!ov) return;
  1339. ov.classList.remove('show');
  1340. setTimeout(()=> { ov.style.display = 'none'; let mc = document.querySelector('#test-overlay .modal-content'); if(mc) mc.classList.remove('pop-animation'); }, 300);
  1341. playAudioCue('click');
  1342. }
  1343. /*
  1344. * АНИМАЦИЯ КРУГА И ПОДСЧЕТА ПРОЦЕНТОВ (COUNT-UP)
  1345. */
  1346. function animateScoreCounter(targetPercent, onFinish) {
  1347. const text = document.getElementById('scoreCircleText');
  1348. const circle = document.getElementById('scoreProgress');
  1349. let color = '#f44336';
  1350. if (targetPercent >= 90) color = '#00E676';
  1351. else if (targetPercent >= 60) color = '#FF9800';
  1352. circle.style.stroke = color;
  1353. text.style.color = color;
  1354. text.style.textShadow = `0 0 15px ${color}`;
  1355. circle.parentElement.style.filter = `drop-shadow(0 0 12px ${color})`;
  1356. const circumference = 2 * Math.PI * 54;
  1357. circle.style.strokeDasharray = `${circumference} ${circumference}`;
  1358. let current = 0; text.innerText = "0%";
  1359. circle.style.strokeDashoffset = circumference;
  1360. setTimeout(() => { circle.style.strokeDashoffset = circumference - (targetPercent / 100) * circumference; }, 50);
  1361. const stepTime = Math.max(15, Math.floor(1200 / (targetPercent || 1)));
  1362. const timer = setInterval(() => {
  1363. if (current < targetPercent) {
  1364. current++; text.innerText = `${current}%`;
  1365. if (current % 4 === 0) playAudioCue('calcKey');
  1366. } else {
  1367. clearInterval(timer); text.innerText = `${targetPercent}%`;
  1368. if (onFinish) onFinish();
  1369. }
  1370. }, stepTime);
  1371. }
  1372. function checkTest(){
  1373. clearInterval(quizTimerInterval);
  1374. const total = quizQuestionsList.length; let score = 0;
  1375. let explanationHTML = `<h4 style='margin-top:0; color:var(--accent); font-size:16px;'>${i18n[currentLang].testReviewTitle}</h4><ul style='padding-left: 18px; line-height:1.6;'>`;
  1376. quizQuestionsList.forEach((item, idx) => {
  1377. const chosenOptIdx = quizAnswers[idx]; const isAnswered = (chosenOptIdx !== undefined); const isCorrect = isAnswered && item.opts[chosenOptIdx].c;
  1378. if (isCorrect) { score++; } else {
  1379. const correctOpt = item.opts.find(o => o.c);
  1380. const skippedTag = !isAnswered ? ` <span style="color:#f44336; font-weight:bold;">[${i18n[currentLang].testSkipped}]</span>` : '';
  1381. explanationHTML += `<li style='margin-bottom: 8px;'><b>№${idx + 1}:</b> ${item.q}${skippedTag}<br><span style='color:#00E676;'>✔ ${correctOpt ? correctOpt.t : ''}</span> — <i>${item.reason}</i> <br><a href="${item.link}" target="_blank" style="color:var(--base-wave); font-size:13px; text-decoration:none;">${i18n[currentLang].wikiLink}</a></li>`;
  1382. }
  1383. });
  1384. explanationHTML += `</ul>`;
  1385. document.getElementById('quiz-card-wrapper').style.display = 'none';
  1386. document.getElementById('quiz-nav-controls').style.display = 'none';
  1387. document.getElementById('quizTimerBar').style.display = 'none';
  1388. const resScreen = document.getElementById('test-result-screen');
  1389. resScreen.style.display = 'block'; resScreen.classList.add('pop-animation');
  1390. let pct = Math.round((score / total) * 100);
  1391. const resText = document.getElementById('test-result-text');
  1392. resText.innerHTML = `${i18n[currentLang].testResTitle} ${score} ${i18n[currentLang].testResOf} ${total}<br>`;
  1393. animateScoreCounter(pct, () => {
  1394. if (pct >= 90) { resText.innerHTML += `<span style="color:#00E676;">${i18n[currentLang].testRes90}</span>`; playAudioCue('glassBell'); }
  1395. else if (pct >= 60) { resText.innerHTML += `<span style="color:#FF9800;">${i18n[currentLang].testRes60}</span>`; playAudioCue('glassBell'); }
  1396. else { resText.innerHTML += `<span style="color:#f44336;">${i18n[currentLang].testResLow}</span>`; playAudioCue('warn'); }
  1397. });
  1398. document.getElementById('quiz-explanation').innerHTML = explanationHTML;
  1399. }
  1400. </script>
  1401. </body>
  1402. </html>