index.html 181 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872
  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 v21.0 | 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. /* УВЕДОМЛЕНИЯ (TOASTS) */
  35. #toast-container { position: fixed; bottom: 25px; right: 25px; z-index: 9999999; display: flex; flex-direction: column; gap: 14px; pointer-events: none; }
  36. .toast { pointer-events: auto; background: rgba(var(--panel-rgb), 0.92); border-left: 5px solid var(--accent); border-radius: 12px; padding: 16px 20px; width: 330px; box-shadow: 0 12px 35px rgba(0,0,0,0.25); backdrop-filter: blur(12px); transform: translateX(120%); transition: transform 0.5s cubic-bezier(0.25, 0.8, 0.25, 1), opacity 0.5s ease; display: flex; gap: 14px; align-items: flex-start; cursor: pointer; position: relative; overflow: hidden; }
  37. .toast.show { transform: translateX(0); opacity: 1; }
  38. .toast:hover { transform: translateX(-5px) scale(1.02); box-shadow: 0 15px 40px var(--btn-glow); }
  39. .toast-icon { font-size: 26px; animation: toastBounce 2s infinite; }
  40. .toast-content { flex-grow: 1; }
  41. .toast-title { font-weight: 900; font-size: 15px; color: var(--accent); margin-bottom: 4px; }
  42. .toast-text { font-size: 13px; line-height: 1.4; color: var(--text-color); font-weight: 500; }
  43. @keyframes toastBounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
  44. /* СИСТЕМА ПОДСКАЗОК (СТРЕЛОЧКИ) */
  45. .onboarding-hint { position: absolute; z-index: 5000; animation: pointBounce 1.5s infinite; filter: drop-shadow(0 4px 10px rgba(0,0,0,0.3)); pointer-events: none; display: flex; flex-direction: column; align-items: center; justify-content: center; }
  46. .onboarding-text { background: var(--accent); color: #fff; padding: 6px 12px; border-radius: 10px; font-size: 12px; font-weight: bold; white-space: nowrap; box-shadow: 0 4px 15px var(--btn-glow); border: 1.5px solid #fff; }
  47. @keyframes pointBounce { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-10px) scale(1.05); } }
  48. /* РЕЙТИНГ ЗВЕЗДЫ */
  49. .star-rating { display: inline-flex; flex-direction: row-reverse; gap: 6px; justify-content: center; margin: 15px 0; }
  50. .star-rating input { display: none; }
  51. .star-rating label { font-size: 38px; color: #ccc; cursor: pointer; transition: 0.2s; text-shadow: 0 2px 4px rgba(0,0,0,0.2); line-height: 1; }
  52. .star-rating label:hover, .star-rating label:hover ~ label, .star-rating input:checked ~ label { color: #FFD700; text-shadow: 0 0 15px rgba(255, 215, 0, 0.7); transform: scale(1.15); }
  53. /* ГЛОБАЛЬНЫЙ RIPPLE ЭФФЕКТ (КРУГИ ПО ВОДЕ) */
  54. .global-ripple { position: fixed; border-radius: 50%; border: 2.5px solid var(--accent); transform: translate(-50%, -50%); pointer-events: none; animation: rippleAnim 0.6s cubic-bezier(0.25, 0.8, 0.25, 1) forwards; z-index: 9999999; box-shadow: 0 0 15px var(--btn-glow); }
  55. @keyframes rippleAnim { 0% { width: 0; height: 0; opacity: 1; border-width: 4px; } 100% { width: 120px; height: 120px; opacity: 0; border-width: 1px; } }
  56. /* ВОЛНОВЫЕ КНОПКИ С КОНТРАСТОМ ДЛЯ ПАНЕЛЕЙ */
  57. .btn, .btn-enter, .premium-link-btn, .lang-btn, .step-btn,
  58. .accordion-btn, .formula-card, .menu-btn-special,
  59. .btn-test-special, .btn-reload-styled, .file-upload-btn,
  60. .glass-controls button, .quiz-option, .tab, .btn-submit-main,
  61. .btn-tzi, .quiz-nav-btn {
  62. border-radius: var(--wave-btn-radius) !important;
  63. transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
  64. border: 2px solid rgba(255,255,255,0.7) !important;
  65. text-shadow: 0 1px 2px rgba(0,0,0,0.4);
  66. }
  67. .btn:hover, .btn-enter:hover, .premium-link-btn:hover, .lang-btn:hover,
  68. .step-btn:hover, .accordion-btn:hover, .formula-card:hover,
  69. .menu-btn-special:hover, .btn-test-special:hover, .btn-reload-styled:hover,
  70. .file-upload-btn:hover, .glass-controls button:hover, .quiz-option:hover,
  71. .tab:hover, .btn-submit-main:hover, .btn-tzi:hover, .quiz-nav-btn:hover {
  72. border-radius: var(--wave-btn-hover) !important;
  73. transform: translateY(-3px); box-shadow: 0 8px 20px var(--btn-glow);
  74. }
  75. .btn-pause, .btn-medium, .premium-link-btn[style*="FF9800"] {
  76. background: linear-gradient(135deg, #FF6F00, #E64A19) !important;
  77. color: #ffffff !important; border: 2px solid rgba(255, 255, 255, 0.85) !important;
  78. box-shadow: 0 4px 14px rgba(230, 74, 25, 0.45) !important; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5) !important;
  79. }
  80. .btn-pause.active { background: linear-gradient(135deg, #D81B60, #AD1457) !important; border-color: #ffffff !important; box-shadow: 0 0 18px rgba(216, 27, 96, 0.7) !important; }
  81. .premium-link-btn.foxford-btn {
  82. background: linear-gradient(135deg, #FF6D00, #DD2C00) !important; color: #ffffff !important; border: 2px solid #ffffff !important;
  83. box-shadow: 0 4px 15px rgba(221, 44, 0, 0.5) !important; text-shadow: 0 1px 3px rgba(0,0,0,0.6) !important;
  84. }
  85. /* УНИКАЛЬНАЯ КНОПКА КАМЕРЫ (СНИМОК) С ИНТЕРАКТИВОМ */
  86. .btn-camera-snap {
  87. display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 8px 18px; font-size: 13px; font-weight: 900; cursor: pointer;
  88. background: linear-gradient(135deg, #651FFF, #3D5AFE) !important; color: #fff !important; border: 2px solid #fff !important;
  89. border-radius: 24px 6px 24px 6px !important; box-shadow: 0 4px 15px rgba(101, 31, 255, 0.4) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.5);
  90. transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); position: relative; overflow: visible; z-index: 10;
  91. }
  92. .btn-camera-snap:hover { border-radius: 6px 24px 6px 24px !important; transform: translateY(-2px) scale(1.05); box-shadow: 0 6px 20px rgba(101, 31, 255, 0.6) !important; }
  93. .btn-camera-snap:active { transform: scale(0.9); }
  94. .camera-flash-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #ffffff; z-index: 10; opacity: 0; pointer-events: none; border-radius: 12px; }
  95. @keyframes flashAnim { 0% { opacity: 0; } 10% { opacity: 1; } 100% { opacity: 0; } }
  96. #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; }
  97. .gradient-text { animation: colorFlow 6s ease infinite; font-weight: bold; }
  98. @keyframes colorFlow { 0% { color: #ff00cc; } 50% { color: #3333ff; } 100% { color: #ff00cc; } }
  99. .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); }
  100. .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; }
  101. .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; }
  102. @keyframes shine { 0% { left: -100%; } 20% { left: 200%; } 100% { left: 200%; } }
  103. #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); }
  104. .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; }
  105. .hint-icon:hover { transform: scale(1.2); filter: brightness(1.1); box-shadow: 0 0 15px rgba(255, 193, 7, 0.9); }
  106. .pulse-alert { animation: pulseAlert 1.5s infinite; }
  107. @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); } }
  108. @media (hover: hover) and (pointer: fine) {
  109. .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); }
  110. .accordion-btn:hover { background: var(--accent); color: white; padding-left: 22px; }
  111. .info-card:hover { transform: translateY(-6px) scale(1.01); box-shadow: 0 15px 35px var(--btn-glow); border-color: var(--accent); z-index: 5; }
  112. .btn-enter:hover { transform: translateY(-4px) scale(1.04) !important; box-shadow: 0 15px 40px var(--btn-glow) !important; }
  113. .interactive-hover:hover { box-shadow: 0 8px 25px var(--btn-glow); }
  114. }
  115. .glass-element:active { transform: scale(0.96) !important; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2) !important; }
  116. .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; }
  117. .btn-enter:active { transform: scale(0.96) !important; }
  118. .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; }
  119. .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); position: relative; }
  120. .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; }
  121. .nav-logo .logo-super-shape.small { width: 36px; height: 36px; animation: morphRing 3s ease-in-out infinite alternate, ringGlow 2s infinite alternate; }
  122. .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; }
  123. .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; position: relative; }
  124. .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; }
  125. .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); }
  126. .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; }
  127. .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); }
  128. .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; }
  129. .logo-super-shape.small { width: 32px; height: 32px; padding: 2px; border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; margin-bottom: 0; }
  130. .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); }
  131. @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%; } }
  132. @keyframes ringGlow { 0% { box-shadow: 0 0 15px var(--btn-glow); } 100% { box-shadow: 0 0 35px var(--accent); } }
  133. #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; }
  134. .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; }
  135. .orb-1 { width: 50vmin; height: 50vmin; background: var(--accent); top: -5%; left: -5%; }
  136. .orb-2 { width: 40vmin; height: 40vmin; background: var(--base-wave); bottom: -5%; right: -5%; animation-delay: -3s; }
  137. @keyframes floatOrb { 0% { transform: translate(0, 0) scale(1); } 100% { transform: translate(50px, 50px) scale(1.1); } }
  138. .app-title { font-size: clamp(34px, 8vw, 64px); margin: 20px 0 5px; line-height: 1.2; letter-spacing: 1px; }
  139. .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; }
  140. #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; }
  141. #sidebar { position: fixed; left: -360px; top: 0; width: 320px; max-width: 85%; height: 100vh; z-index: 3000; transition: left 0.4s 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); }
  142. #sidebar.open { left: 0; }
  143. #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); }
  144. #sidebar-overlay.show { display: block; }
  145. .sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 1px solid var(--border-color); padding-bottom: 10px; }
  146. .sidebar-header h2 .emoji-fix { display: inline-block; transition: transform 0.6s cubic-bezier(0.25, 0.8, 0.25, 1); }
  147. .sidebar-header:hover h2 .emoji-fix { transform: rotate(180deg) scale(1.2); }
  148. .close-sidebar { font-size: 34px; cursor: pointer; color: var(--text-color); transition: 0.2s; padding: 5px; }
  149. .close-sidebar:hover { color: var(--accent); transform: scale(1.1); }
  150. .sidebar-section { margin-bottom: 20px; }
  151. .sidebar-title { font-size: 13px; text-transform: uppercase; color: var(--accent); margin-bottom: 10px; font-weight: bold; letter-spacing: 0.5px; }
  152. /* ИНТЕРАКТИВНЫЕ КНОПКИ МЕНЮ С ЭФФЕКТОМ ЗАЛИВКИ */
  153. .sidebar-btn-asym {
  154. padding: 12px 16px; margin-bottom: 8px; font-weight: bold; display: flex; align-items: center; gap: 10px;
  155. font-size: 14px; border-left: 4px solid var(--accent); cursor: pointer;
  156. transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); position: relative; overflow: hidden; z-index: 1;
  157. }
  158. .sidebar-btn-asym::before {
  159. content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
  160. background: linear-gradient(90deg, transparent, var(--accent)); transition: all 0.4s ease; z-index: -1;
  161. }
  162. .sidebar-btn-asym:hover { transform: translateX(8px) scale(1.02); color: white; border-color: white; box-shadow: 0 4px 15px var(--btn-glow); }
  163. .sidebar-btn-asym:hover::before { left: 0; }
  164. .setting-block { margin-bottom: 15px; }
  165. .setting-block label { display: block; margin-bottom: 6px; font-weight: bold; font-size: 14px; }
  166. .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; transition: 0.3s; cursor: pointer; }
  167. .setting-block select:hover { border-color: var(--accent); box-shadow: 0 0 10px var(--btn-glow); }
  168. .file-upload-wrapper { position: relative; overflow: hidden; display: block; margin-bottom: 8px; }
  169. .file-upload-input { position: absolute; left: 0; top: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; z-index: 2; }
  170. .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; }
  171. .glass-controls { display: flex; gap: 6px; margin-bottom: 10px; }
  172. .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); }
  173. .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); }
  174. .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; position: relative; }
  175. input[type=range] { width: 100%; margin-top: 5px; accent-color: var(--accent); cursor: pointer; transition: 0.2s; }
  176. input[type=range]:hover { transform: scale(1.02); filter: brightness(1.2); }
  177. .grid-container { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 16px; min-height: 75vh; margin-bottom: 30px; }
  178. .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; }
  179. .panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; flex-shrink: 0; flex-wrap: wrap; gap: 8px; }
  180. /* 3D ИНТЕРАКТИВ ДЛЯ ГРАФИКОВ */
  181. .canvas-wrapper { flex-grow: 1; position: relative; width: 100%; height: 260px; min-height: 160px; resize: both; overflow: hidden; border-radius: 12px; border: 2px solid var(--element-border); background: var(--canvas-bg); transition: transform 0.3s ease, box-shadow 0.3s ease; }
  182. .canvas-wrapper:hover { transform: scale(1.01); box-shadow: 0 12px 35px var(--btn-glow); z-index: 10; border-color: var(--accent); }
  183. canvas { display: block; position: absolute; top: 0; left: 0; width: 100% !important; height: 100% !important; }
  184. .chart-telemetry { position: absolute; bottom: 8px; left: 8px; z-index: 5; display: flex; flex-wrap: wrap; gap: 6px; pointer-events: none; }
  185. .chart-telemetry.hidden { display: none; }
  186. .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; transition: 0.3s; }
  187. .telemetry-chip .val-highlight { color: var(--accent); font-weight: 900; font-size: 13px; }
  188. .telemetry-chip .val-fixed { display: inline-block; min-width: 42px; text-align: right; }
  189. .tabs { display: flex; gap: 8px; margin: 8px 0; flex-shrink: 0; width: 100%; }
  190. .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); }
  191. .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); }
  192. .presets-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 8px; width: 100%; }
  193. .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; }
  194. .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); }
  195. .control-row { margin-bottom: 11px; display: flex; align-items: center; flex-shrink: 0; gap: 10px; width: 100%; }
  196. .control-label { width: 135px; font-weight: bold; font-size: 14px; flex-shrink: 0; display: flex; justify-content: space-between; align-items: center; }
  197. .control-body { display: flex; align-items: center; flex-grow: 1; gap: 10px; width: 100%; }
  198. .secondary-params { background: rgba(var(--panel-rgb), 0.5); border: 2px dashed var(--element-border); border-radius: 14px; padding: 12px; margin-top: 8px; }
  199. .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; }
  200. .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; }
  201. .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); }
  202. .input-wrapper { display: flex; align-items: center; gap: 5px; flex-shrink: 0; }
  203. .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; }
  204. .value-input:focus { box-shadow: 0 0 15px var(--btn-glow); background: rgba(var(--panel-rgb), 0.98); transform: scale(1.08); border-color: var(--accent-alt); color: var(--accent-alt); }
  205. .slider-alt { accent-color: var(--accent-alt) !important; }
  206. .input-alt { border-color: var(--accent-alt) !important; color: var(--accent-alt) !important; background: rgba(230, 74, 25, 0.12) !important; }
  207. .btn-group { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
  208. .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); }
  209. .btn-audio { background: linear-gradient(135deg, #0288D1, #01579B) !important; box-shadow: 0 4px 12px rgba(2, 136, 209, 0.4) !important; }
  210. .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; }
  211. .btn-dist { background: linear-gradient(135deg, #8E24AA, #4A148C) !important; box-shadow: 0 4px 12px rgba(142, 36, 170, 0.4) !important; }
  212. .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; }
  213. .btn-reset { background: linear-gradient(135deg, #546E7A, #37474F) !important; box-shadow: 0 4px 12px rgba(84, 110, 122, 0.4) !important; }
  214. .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); }
  215. .content-header { text-align: center; margin-bottom: 35px; font-size: clamp(22px, 5vw, 28px); font-weight: 800; }
  216. .info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 30px; }
  217. .info-card { padding: 22px; background: rgba(var(--panel-rgb), 0.85); border: 2px solid var(--border-color); border-radius: 16px; }
  218. .info-card h3 { color: var(--accent); margin-top: 0; font-size: 18px; font-weight: 900; }
  219. .info-card p { line-height: 1.6; font-size: 15px; margin-bottom: 0; }
  220. .key-term { font-weight: 800; letter-spacing: 0.5px; color: var(--accent); text-decoration: underline dotted; }
  221. .faq-link { color: var(--base-wave); text-decoration: none; font-weight: bold; transition: color 0.2s; display: inline-block; margin-top: 6px; }
  222. .faq-link:hover { color: var(--accent); text-decoration: underline; }
  223. .calc-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 40px; }
  224. .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; transition: 0.4s; }
  225. .wave-calculator:hover { transform: translateY(-5px); border-color: #00E676; box-shadow: 0 15px 40px rgba(0, 230, 118, 0.2); }
  226. .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; transition: 0.3s; }
  227. .calc-formula { font-family: 'Courier New', monospace; font-size: 18px; color: #81c784; margin-bottom: 8px; font-weight: bold; }
  228. .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; }
  229. .calc-controls { display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
  230. .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; }
  231. .calc-input:focus { border-color: #00E676; box-shadow: 0 0 15px rgba(0, 230, 118, 0.5); transform: scale(1.08); }
  232. .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; }
  233. .calc-select:focus { border-color: #00E676; box-shadow: 0 0 15px rgba(0,230,118,0.5); }
  234. .accordion { width: 100%; margin-top: 15px; }
  235. .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; position: relative; overflow: hidden; z-index: 1;}
  236. .accordion-btn::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(156, 39, 176, 0.2)); transition: all 0.4s ease; z-index: -1; }
  237. .accordion-btn:hover::before { left: 0; }
  238. .accordion-btn.active { background: var(--accent); color: #fff; border-bottom-left-radius: 0; border-bottom-right-radius: 0; margin-bottom: 0; }
  239. .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; }
  240. .accordion-content p { padding: 16px 0; line-height: 1.6; margin: 0; font-size: 15px; }
  241. .resource-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; margin-top: 20px; }
  242. .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); }
  243. .premium-link-btn.sim-btn { background: linear-gradient(135deg, #00C853, #00796B); }
  244. .premium-link-btn.wiki-btn { background: linear-gradient(135deg, #1976D2, #0D47A1); }
  245. .premium-link-btn.youtube-btn { background: linear-gradient(135deg, #E53935, #B71C1C); }
  246. .premium-link-btn.ru-btn { background: linear-gradient(135deg, #303F9F, #1A237E); }
  247. .premium-link-btn.rutube-btn { background: linear-gradient(135deg, #111, #222); border-color: #00D1FF !important; }
  248. .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; }
  249. .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; }
  250. .modal-overlay.show { opacity: 1; display: flex; }
  251. .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); }
  252. .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; }
  253. .close-btn:hover { transform: scale(1.15) rotate(90deg); color: var(--accent); }
  254. .about-section { background: rgba(var(--panel-rgb), 0.8); border: 1.5px solid var(--border-color); padding: 20px; margin-bottom: 18px; border-radius: 16px; }
  255. .about-section h3 { margin-top: 0; color: var(--accent); display: flex; align-items: center; gap: 10px; font-size: 18px; }
  256. .about-section p { line-height: 1.6; font-size: 15px; margin-bottom: 0; }
  257. /* КРАСИВЫЙ ЖУРНАЛ ИЗМЕНЕНИЙ (CHANGELOG) */
  258. .timeline { border-left: 3px solid var(--accent); padding-left: 18px; margin-left: 10px; position: relative; margin-top: 15px; }
  259. .timeline-item { position: relative; margin-bottom: 22px; font-size: 14px; line-height: 1.6; background: rgba(var(--panel-rgb), 0.6); padding: 15px; border-radius: 10px; border: 1.5px solid var(--border-color); transition: 0.3s; }
  260. .timeline-item:hover { background: rgba(var(--panel-rgb), 0.95); transform: translateX(8px) scale(1.01); box-shadow: 0 8px 25px var(--btn-glow); border-color: var(--accent); }
  261. .timeline-item::before { content: ''; position: absolute; left: -26px; top: 18px; width: 14px; height: 14px; border-radius: 50%; background: var(--accent); border: 3px solid var(--bg-color); box-shadow: 0 0 10px var(--btn-glow); transition: 0.3s; }
  262. .timeline-item:hover::before { transform: scale(1.3); background: #fff; border-color: var(--accent); }
  263. .timeline-item h4 { color: var(--accent); display: block; font-size: 16px; margin: 0 0 8px 0; font-weight: 900; }
  264. .timeline-item ul { margin: 0; padding-left: 20px; color: var(--text-color); font-weight: 500; list-style-type: none; }
  265. .timeline-item ul li { margin-bottom: 5px; position: relative; }
  266. .timeline-item ul li::before { content: "🔹"; position: absolute; left: -22px; font-size: 12px; top: 2px; }
  267. .formula-grid { display: grid; grid-template-columns: 1fr; gap: 12px; margin-bottom: 24px; }
  268. @media (min-width: 600px) { .formula-grid { grid-template-columns: 1fr 1fr; } }
  269. .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; }
  270. .formula-card:hover { transform: translateY(-5px) scale(1.02); box-shadow: 0 8px 25px var(--btn-glow); border-color: var(--accent); z-index: 2; }
  271. .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; }
  272. /* ИНТЕРАКТИВНЫЕ ТЕСТЫ И СЛАЙДЕР С SVG АНИМАЦИЕЙ */
  273. .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; }
  274. .timer-progress-bg { width: 100%; height: 8px; background: rgba(255, 255, 255, 0.12); position: relative; }
  275. .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; }
  276. .timer-content { display: flex; justify-content: space-between; align-items: center; width: 100%; padding: 10px 20px; position: relative; z-index: 2; }
  277. .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); }
  278. .timer-orange { color: #FF9800 !important; text-shadow: 0 0 10px rgba(255, 152, 0, 0.6); }
  279. .timer-red-blink { color: #f44336 !important; animation: timerBlink 1s infinite; text-shadow: 0 0 15px rgba(244, 67, 54, 0.8); }
  280. @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); } }
  281. .test-modes { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; margin-top: 25px; }
  282. .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; }
  283. .btn-easy { background: linear-gradient(135deg, #4CAF50, #2E7D32) !important; box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3) !important; }
  284. .btn-easy:hover { box-shadow: 0 0 25px rgba(76, 175, 80, 0.9) !important; border-color: #fff !important; filter: brightness(1.15); transform: translateY(-3px) scale(1.02); }
  285. .btn-medium { background: linear-gradient(135deg, #FF9800, #EF6C00) !important; box-shadow: 0 4px 15px rgba(255, 152, 0, 0.3) !important; }
  286. .btn-medium:hover { box-shadow: 0 0 25px rgba(255, 152, 0, 0.9) !important; border-color: #fff !important; filter: brightness(1.15); transform: translateY(-3px) scale(1.02); }
  287. .btn-hard { background: linear-gradient(135deg, #F44336, #C62828) !important; box-shadow: 0 4px 15px rgba(244, 67, 54, 0.3) !important; }
  288. .btn-hard:hover { box-shadow: 0 0 25px rgba(244, 67, 54, 0.9) !important; border-color: #fff !important; filter: brightness(1.15); transform: translateY(-3px) scale(1.02); }
  289. .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; }
  290. .btn-ultra:hover { box-shadow: 0 0 25px rgba(233, 30, 99, 0.9) !important; border-color: #fff !important; filter: brightness(1.15); transform: translateY(-3px) scale(1.02); }
  291. .question-card-wrapper { margin-top: 10px; min-height: 250px; }
  292. .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; }
  293. @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
  294. .question-meta { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; font-weight: 900; font-size: 15px; color: var(--accent); }
  295. .question p { font-weight: bold; margin-top: 0; margin-bottom: 20px; font-size: 18px; line-height: 1.5; color: var(--text-color); }
  296. .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; }
  297. .quiz-option input { display: none; }
  298. .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; }
  299. .quiz-option.selected { border-color: var(--accent); background: rgba(156, 39, 176, 0.15); transform: scale(1.03); box-shadow: 0 8px 20px rgba(156, 39, 176, 0.3); border-radius: var(--wave-btn-hover) !important; z-index: 2;}
  300. .quiz-option.selected .custom-radio { border-color: var(--accent); box-shadow: 0 0 15px var(--btn-glow); }
  301. .quiz-option.selected .custom-radio::after { content: ''; position: absolute; top: 4px; left: 4px; width: 10px; height: 10px; background: var(--accent); border-radius: 50%; }
  302. .quiz-option.selected .option-text { color: var(--accent); }
  303. .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); transform: scale(1.02); }
  304. .quiz-option.correct-answer::after { content: '✔'; position: absolute; right: 16px; color: #2E7D32; font-size: 20px; font-weight: 900; }
  305. .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); transform: scale(1.02); }
  306. .quiz-option.wrong-answer::after { content: '✖'; position: absolute; right: 16px; color: #C62828; font-size: 20px; font-weight: 900; }
  307. .quiz-nav-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-top: 15px; }
  308. .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); }
  309. .quiz-nav-btn:disabled { opacity: 0.35; cursor: not-allowed; transform: none !important; box-shadow: none !important; }
  310. #test-result-screen { display: none; text-align: center; padding: 15px 5px; }
  311. .score-circle-wrapper { position: relative; width: 140px; height: 140px; margin: 0 auto 20px auto; }
  312. .circular-progress { transform: rotate(-90deg); width: 140px; height: 140px; filter: drop-shadow(0 0 10px var(--accent)); transition: filter 0.5s; }
  313. .circular-progress circle { fill: none; stroke-width: 10; stroke-linecap: round; }
  314. .circular-progress .bg { stroke: rgba(var(--panel-rgb), 0.5); }
  315. .circular-progress .progress { stroke: var(--accent); stroke-dasharray: 339.292; stroke-dashoffset: 339.292; transition: stroke-dashoffset 1s ease-out, stroke 0.5s; }
  316. .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; }
  317. .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; box-shadow: inset 0 0 15px rgba(0,0,0,0.05); }
  318. .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; }
  319. .btn-submit-main:active { transform: scale(0.95); }
  320. @media screen and (max-width: 820px) {
  321. #main-app { padding: calc(max(10px, env(safe-area-inset-top)) + 70px) 10px 20px 10px; }
  322. .grid-container { display: flex; flex-direction: column; gap: 16px; margin-bottom: 20px; }
  323. .panel { padding: 16px; }
  324. .canvas-wrapper { height: 260px; min-height: 160px; }
  325. .presets-row { grid-template-columns: repeat(4, 1fr); gap: 4px; }
  326. .presets-row button { padding: 10px 2px; font-size: 11px; }
  327. .control-row { flex-direction: column; align-items: flex-start; gap: 8px; margin-bottom: 12px; }
  328. .control-label { width: 100%; }
  329. .control-body { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
  330. .btn-group { flex-direction: column; gap: 10px; margin-top: 10px; }
  331. .btn { width: 100%; padding: 14px; font-size: 15px; }
  332. .info-grid, .calc-grid { grid-template-columns: 1fr; }
  333. .site-content { padding: 18px; }
  334. .modal-content { padding: 20px; }
  335. .test-modes { grid-template-columns: 1fr; }
  336. .hide-mobile { display: none; }
  337. .btn-reload-styled { display: flex; }
  338. }
  339. </style>
  340. </head>
  341. <body style="background-color: var(--bg-color);">
  342. <div id="bg-layer"></div>
  343. <div id="sidebar-overlay" onclick="toggleSidebar()"></div>
  344. <div id="global-tooltip"></div>
  345. <!-- КОНТЕЙНЕР ДЛЯ НОВЫХ УВЕДОМЛЕНИЙ -->
  346. <div id="toast-container"></div>
  347. <div id="welcome-screen">
  348. <div class="welcome-bg-grid"></div>
  349. <div class="welcome-orb orb-1"></div>
  350. <div class="welcome-orb orb-2"></div>
  351. <div class="logo-super-shape">
  352. <div class="logo-inner-content"><span class="emoji-fix" style="font-size: 44px;">〽️</span></div>
  353. </div>
  354. <h1 class="app-title gradient-text" data-i18n="appTitle" style="margin-top:20px;">HarmoniX</h1>
  355. <h2 class="app-subtitle" data-i18n="appSubtitle">Интерактивная Лаборатория Волновой Физики</h2>
  356. <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 25px rgba(156, 39, 176, 0.45);">
  357. <span class="emoji-fix">✨</span> <span data-i18n="hintYellow">Версия 21.0 Nova | Максимальный интерактив!</span>
  358. </div>
  359. <button class="btn-enter glass-element interactive-hover btn-shine" onclick="startApp()"><span class="emoji-fix">▶</span> <span data-i18n="btnStart">Начать работу</span></button>
  360. </div>
  361. <!-- МЕНЮ НАСТРОЕК -->
  362. <div id="sidebar" class="glass-panel">
  363. <div class="sidebar-header">
  364. <h2 class="gradient-text" style="margin:0; font-size: 18px;"><span class="emoji-fix">⚙</span> <span data-i18n="menuSettings">Настройки</span></h2>
  365. <div class="close-sidebar" onclick="toggleSidebar(); playAudioCue('menuClick');">&times;</div>
  366. </div>
  367. <div class="sidebar-section">
  368. <h3 class="sidebar-title"><span class="emoji-fix">🌐</span> Язык / Language</h3>
  369. <div class="glass-controls">
  370. <button class="lang-btn lang-ru active interactive-hover" onclick="setLang('ru'); playAudioCue('menuClick');">🇷🇺 Русский</button>
  371. <button class="lang-btn lang-en interactive-hover" onclick="setLang('en'); playAudioCue('menuClick');">🇬🇧 English</button>
  372. </div>
  373. </div>
  374. <div class="sidebar-section">
  375. <h3 class="sidebar-title"><span class="emoji-fix">📌</span> <span data-i18n="navHeader">Разделы и Инфо</span></h3>
  376. <div class="sidebar-btn-asym glass-element" onclick="toggleSidebar(); window.scrollTo({top: 0, behavior: 'smooth'}); playAudioCue('menuClick');">🏠 <span data-i18n="navMain">Главная панель</span></div>
  377. <div class="sidebar-btn-asym glass-element" onclick="openTestMenu(); toggleSidebar(); playAudioCue('menuClick');">🎓 <span data-i18n="navTest">Итоговое Тестирование</span></div>
  378. <div class="sidebar-btn-asym glass-element" onclick="showInfoModal('formulas'); toggleSidebar(); playAudioCue('menuClick');">📚 <span data-i18n="navFormulas">Справочник формул</span></div>
  379. <div class="sidebar-btn-asym glass-element" onclick="showInfoModal('about'); toggleSidebar(); playAudioCue('menuClick');">ℹ️ <span data-i18n="navAbout">О платформе</span></div>
  380. </div>
  381. <div class="sidebar-section">
  382. <h3 class="sidebar-title"><span class="emoji-fix">⚡</span> <span data-i18n="lblGraphTools">Инструменты графиков</span></h3>
  383. <div class="glass-controls">
  384. <button id="btnLabelsToggle" class="btn-glass-on active glass-element" onclick="toggleLabels(); playAudioCue('menuClick');"><span data-i18n="lblLabels">Подписи: Вкл</span></button>
  385. <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>
  386. </div>
  387. <div class="glass-controls">
  388. <button id="btnGridToggle" class="btn-glass-on active glass-element" onclick="toggleGrid(); playAudioCue('menuClick');"><span data-i18n="lblGrid">Сетка: Вкл</span></button>
  389. <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>
  390. </div>
  391. <div style="margin-top: 10px;">
  392. <label style="font-size:11px; font-weight:bold; display:block;" data-i18n="lblHeightResize">Высота холстов графиков:</label>
  393. <input type="range" id="canvasHeightSlider" min="160" max="450" value="260" oninput="setCanvasHeight(this.value)">
  394. </div>
  395. </div>
  396. <div class="sidebar-section">
  397. <h3 class="sidebar-title"><span class="emoji-fix">🧊</span> <span data-i18n="lblGlass">Жидкое стекло</span></h3>
  398. <div class="glass-controls">
  399. <button id="btnGOn" class="btn-glass-on active glass-element" onclick="setGlassState(true); playAudioCue('menuClick');">Вкл</button>
  400. <button id="btnGOff" class="btn-glass-on glass-element" style="background: rgba(var(--panel-rgb), 0.3);" onclick="setGlassState(false); playAudioCue('menuClick');">Выкл</button>
  401. <button class="btn-glass-auto glass-element btn-shine" onclick="autoGlass(); playAudioCue('menuClick');"><span class="emoji-fix">🤖</span> Авто</button>
  402. </div>
  403. <input type="range" id="glassIntensity" min="0" max="100" value="70" oninput="updateGlassIntensity()">
  404. </div>
  405. <div class="sidebar-section">
  406. <h3 class="sidebar-title"><span class="emoji-fix">🎨</span> <span data-i18n="lblTheme">Оформление</span></h3>
  407. <div class="setting-block">
  408. <label id="lblThemeSelect" data-i18n="lblThemeSelect">Тема оформления:</label>
  409. <select id="themeSelect" class="emoji-fix glass-element" onchange="changeTheme(this.value); playAudioCue('menuClick');"></select>
  410. </div>
  411. <div class="setting-block">
  412. <label id="lblCustomBg" data-i18n="lblCustomBg">Свои обои:</label>
  413. <div class="file-upload-wrapper glass-element">
  414. <input type="file" accept="image/*" id="bgUpload" class="file-upload-input" onchange="uploadBackground(event)">
  415. <div class="file-upload-btn" data-i18n="btnPickImg">📂 Выбрать картинку</div>
  416. </div>
  417. <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>
  418. </div>
  419. <div class="setting-block">
  420. <label id="lblBgFilter" data-i18n="lblBgFilter">Фильтр фона:</label>
  421. <select class="emoji-fix glass-element" id="bgFilter" onchange="changeFilter(this.value); playAudioCue('menuClick');"></select>
  422. </div>
  423. </div>
  424. <div style="flex-grow: 1;"></div>
  425. <div style="text-align: center; font-size: 12px; opacity: 0.7; font-weight: bold; color: var(--accent);">v21.0 Enterprise Nova</div>
  426. </div>
  427. <!-- ВЕРХНЯЯ ПАНЕЛЬ -->
  428. <div class="top-bar-wrapper" id="main-nav-bar">
  429. <div class="top-bar glass-panel pop-animation">
  430. <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>
  431. <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>
  432. <div class="nav-logo interactive-hover" onclick="window.scrollTo({top: 0, behavior: 'smooth'}); cursor: pointer;">
  433. <div class="logo-super-shape small"><div class="logo-inner-content"><span class="emoji-fix" style="font-size:14px;">〽️</span></div></div>
  434. <span class="hide-mobile gradient-text">HarmoniX</span>
  435. </div>
  436. <div style="position: relative;">
  437. <button id="btnMainTestMode" class="btn-test-special interactive-hover" onclick="openTestMenu(); playAudioCue('glassBell');"><span class="emoji-fix">🎓</span> <span data-i18n="btnTest">Тесты</span></button>
  438. </div>
  439. <button class="btn-fullscreen-compact interactive-hover" onclick="toggleFullscreen(); playAudioCue('glassBell');" title="Full Screen"><span class="emoji-fix">🔲</span></button>
  440. </div>
  441. </div>
  442. <!-- ОСНОВНОЙ КОНТЕНТ И ГРАФИКИ -->
  443. <div id="main-app">
  444. <div class="grid-container">
  445. <div class="panel glass-panel pop-animation" style="animation-delay: 0.05s;">
  446. <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>
  447. <div class="canvas-wrapper glass-element" ondblclick="resetCanvasSizes()">
  448. <canvas id="canvas-single"></canvas>
  449. <div class="chart-telemetry" id="telemSingle">
  450. <div class="telemetry-chip"><span data-i18n="telWave">Волна</span>: <span id="telWaveName" class="val-highlight">V1</span></div>
  451. <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>
  452. <div class="telemetry-chip"><span data-i18n="telA">Амплитуда</span>: <span id="telAmp" class="val-highlight">80</span>%</div>
  453. <div class="telemetry-chip"><span data-i18n="lblPhaseShort">Фаза</span>: <span id="telPhase" class="val-highlight">0°</span></div>
  454. </div>
  455. </div>
  456. </div>
  457. <div class="panel glass-panel pop-animation" style="animation-delay: 0.1s;">
  458. <div class="panel-header">
  459. <h3 style="margin:0; font-size:16px;" data-i18n="pnl3">3. Теорема Фурье (Сумма)</h3>
  460. <div style="position: relative; z-index: 100;">
  461. <button id="btnMainSnap" class="btn-camera-snap" onclick="downloadGraph('canvas-sum'); playAudioCue('cameraSnap');">
  462. <span class="emoji-fix">📸</span> <span data-i18n="btnSaveCompact">Снимок</span>
  463. </button>
  464. </div>
  465. </div>
  466. <div class="canvas-wrapper glass-element" ondblclick="resetCanvasSizes()">
  467. <canvas id="canvas-sum"></canvas>
  468. <div class="chart-telemetry" id="telemSum">
  469. <div class="telemetry-chip"><span data-i18n="telClip">Нелинейное</span>: <span id="telClipStatus" class="val-highlight">Выкл</span></div>
  470. <div class="telemetry-chip" id="telemTHD" style="display:none;"><span data-i18n="telTHD">THD (Искажения)</span>: <span id="telTHDVal" class="val-highlight">0%</span></div>
  471. <div class="telemetry-chip" id="telemLimit" style="display:none;"><span data-i18n="telLimit">Порог среза</span>: <span id="telLimitVal" class="val-highlight">±25%</span></div>
  472. </div>
  473. </div>
  474. </div>
  475. <div class="panel glass-panel pop-animation" style="animation-delay: 0.15s;">
  476. <div class="panel-header">
  477. <h3 style="margin:0; font-size:16px;" data-i18n="pnl2">2. Панель Управления</h3>
  478. <div class="presets-row">
  479. <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>
  480. <button class="glass-element interactive-hover" id="pr_square" onclick="setPreset('square')"><span class="emoji-fix">🟦</span> <span data-i18n="btnSquare">Меандр</span></button>
  481. <button class="glass-element interactive-hover" id="pr_triangle" onclick="setPreset('triangle')"><span class="emoji-fix">📐</span> <span data-i18n="btnTriangle">Треуг.</span></button>
  482. <button class="glass-element interactive-hover" id="pr_sawtooth" onclick="setPreset('sawtooth')"><span class="emoji-fix">🔪</span> <span data-i18n="btnSaw">Пила</span></button>
  483. </div>
  484. </div>
  485. <div class="tabs">
  486. <div class="tab glass-element active interactive-hover" onclick="switchTab(0); playAudioCue('mainPulse');">V1</div>
  487. <div class="tab glass-element interactive-hover" onclick="switchTab(1); playAudioCue('mainPulse');">V2</div>
  488. <div class="tab glass-element interactive-hover" onclick="switchTab(2); playAudioCue('mainPulse');">V3</div>
  489. </div>
  490. <div class="control-row">
  491. <div class="control-label" data-i18n="lblStep">Шаг частоты:</div>
  492. <div class="control-body">
  493. <div class="step-toggle">
  494. <div class="step-btn interactive-hover" id="st10" onclick="updateStep(10); playAudioCue('mainPulse');">10 <span class="unit-freq" data-i18n="hz">Гц</span></div>
  495. <div class="step-btn active interactive-hover" id="st100" onclick="updateStep(100); playAudioCue('mainPulse');">100 <span class="unit-freq" data-i18n="hz">Гц</span></div>
  496. </div>
  497. </div>
  498. </div>
  499. <div class="control-row">
  500. <div class="control-label" data-i18n="lblFreq">Частота (Гц):</div>
  501. <div class="control-body">
  502. <input type="range" id="freq" min="0" max="20000" step="100" value="440" oninput="updateWaveFromSlider('freq')">
  503. <div class="input-wrapper"><input type="number" id="inputV" class="value-input glass-element" value="440" min="0" max="20000" oninput="updateWaveFromInput('freq')"></div>
  504. </div>
  505. </div>
  506. <div class="control-row">
  507. <div class="control-label" data-i18n="lblAmp">Амплитуда (%):</div>
  508. <div class="control-body">
  509. <input type="range" id="amp" min="0" max="100" value="80" oninput="updateWaveFromSlider('amp')">
  510. <div class="input-wrapper"><input type="number" id="inputA" class="value-input glass-element" value="80" min="0" max="100" oninput="updateWaveFromInput('amp')"></div>
  511. </div>
  512. </div>
  513. <div class="secondary-params glass-element">
  514. <div class="control-row" style="margin-bottom: 8px;">
  515. <div class="control-label"><span data-i18n="lblPhase">Сдвиг фазы (°)</span><span class="hint-icon" data-i18n-hint="hintPhase" data-hint="Сдвиг фазы смещает волну во времени. Сдвиг на 180° создает инвертированную волну (противофазу).">?</span></div>
  516. <div class="control-body">
  517. <input type="range" class="slider-alt" id="phase" min="0" max="360" step="1" value="0" oninput="updateWaveFromSlider('phase')">
  518. <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>
  519. </div>
  520. </div>
  521. <div class="control-row" style="margin-bottom: 0;">
  522. <div class="control-label"><span data-i18n="lblSpeed">Скорость волны</span><span class="hint-icon" data-i18n-hint="hintSpeedGraph" data-hint="Управление скоростью анимации на графиках. Не меняет физическую частоту колебаний.">?</span></div>
  523. <div class="control-body">
  524. <input type="range" class="slider-alt" id="speed" min="0.1" max="3" step="0.1" value="1" oninput="updateSpeedValue(this.value);">
  525. <div class="input-wrapper"><div class="value-input input-alt glass-element" id="valSpeedDisp" style="line-height: 20px;">1.0x</div></div>
  526. </div>
  527. </div>
  528. </div>
  529. <div class="btn-group">
  530. <button class="btn btn-audio interactive-hover" onclick="toggleAudio(); playAudioCue('mainPulse');" id="btnAudio"><span class="emoji-fix">🔊</span> <span data-i18n="btnAudio">Динамик</span></button>
  531. <button class="btn btn-pause interactive-hover" onclick="togglePause(); playAudioCue('mainPulse');" id="btnPause"><span class="emoji-fix">⏸</span> <span data-i18n="btnPause">Стоп-кадр</span></button>
  532. <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;">
  533. <span class="emoji-fix">⚡</span>
  534. <span data-i18n="btnClip">Нелинейн. преобр.</span>
  535. <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>
  536. </button>
  537. <button class="btn btn-reset interactive-hover" onclick="resetAll(); playAudioCue('mainPulse');"><span class="emoji-fix">🔄</span> <span data-i18n="btnReset">Сброс</span></button>
  538. </div>
  539. </div>
  540. <div class="panel glass-panel pop-animation" style="animation-delay: 0.2s;">
  541. <h3 style="margin:0; margin-bottom: 8px; font-size:16px;" data-i18n="pnl4">4. Спектрограмма</h3>
  542. <div class="canvas-wrapper glass-element" ondblclick="resetCanvasSizes()">
  543. <canvas id="canvas-spectrum"></canvas>
  544. <div class="chart-telemetry" id="telemSpec">
  545. <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>
  546. <div class="telemetry-chip"><span data-i18n="telPeaks">Пики</span>: <span id="telSpecPeaks" class="val-highlight">1</span></div>
  547. </div>
  548. </div>
  549. </div>
  550. <div class="panel glass-panel pop-animation" style="grid-column: 1 / -1; border-color: var(--tzi-color); animation-delay: 0.25s;">
  551. <div class="panel-header">
  552. <h3 style="color: var(--tzi-color); margin: 0;"><span class="emoji-fix">🛡️</span> <span data-i18n="tziTitle">ТЗИ: Виброакустическая защита</span></h3>
  553. <span class="hint-icon" style="background:var(--tzi-color); color:#000; margin: 0;" data-i18n-hint="hintTzi" data-hint="Симуляция технической защиты информации.">?</span>
  554. </div>
  555. <p style="font-size:15px; margin-bottom:15px; margin-top: 10px; font-weight: bold;" data-i18n="tziDesc">Активация генератора шума для маскировки речевой информации от средств перехвата:</p>
  556. <div style="display:flex; justify-content:center; position: relative;">
  557. <button class="btn btn-tzi interactive-hover" id="btnTziMode" style="width:100%; max-width:400px; padding:14px; font-size:16px; font-weight:900; position:relative; z-index:100;" onclick="toggleTZI()"><span class="emoji-fix">🔒</span> <span data-i18n="btnTziStart">Активировать маскировку</span></button>
  558. </div>
  559. </div>
  560. </div>
  561. <div class="site-content glass-panel pop-animation" style="animation-delay: 0.3s;">
  562. <h2 class="content-header gradient-text" data-i18n="eduHeader">📚 Академия Волновой Механики</h2>
  563. <div class="info-grid">
  564. <div class="info-card glass-element interactive-hover">
  565. <h3 data-i18n="eduCard1Title"><span class="emoji-fix">〰️</span> Природа механических волн</h3>
  566. <p data-i18n="eduCard1Text">Механическая волна представляет собой процесс распространения колебательного возмущения в упругой среде. При этом частицы среды лишь совершают малые колебания возле равновесия, а переносится исключительно <b>ЭНЕРГИЯ И ИМПУЛЬС</b>.</p>
  567. </div>
  568. <div class="info-card glass-element interactive-hover">
  569. <h3 data-i18n="eduCard2Title"><span class="emoji-fix">📉</span> Теорема и спектральный анализ Фурье</h3>
  570. <p data-i18n="eduCard2Text1">В 1822 году Жан-Батист Фурье доказал: <b>ЛЮБОЙ ПЕРИОДИЧЕСКИЙ СИГНАЛ</b> сложной формы может быть разложен в ряд простых гармонических волн кратных частот.</p>
  571. </div>
  572. </div>
  573. <div class="calc-grid">
  574. <div class="wave-calculator">
  575. <h3 style="margin-top:0; color:#fff;" data-i18n="calcTitle"><span class="emoji-fix">🧮</span> Калькулятор длины волны</h3>
  576. <div class="calc-screen">
  577. <div class="calc-formula">λ = v / ν</div>
  578. <div style="font-size: 13px; color: #81c784; margin-bottom: 5px;" id="lblCalcAirDesc" data-i18n="calcAirDesc">В воздухе (343 м/с):</div>
  579. <div class="calc-result"><span id="calcResAir">0.77</span> <span class="calc-m" data-i18n="meters">м</span></div>
  580. </div>
  581. <div class="calc-controls">
  582. <span style="font-weight: bold; font-size: 14px;"><span data-i18n="lblFreqShort">ν (Гц)</span>:</span>
  583. <input type="number" id="calcFreq1" class="calc-input" value="440" min="1" oninput="calculateWaveLength(); playAudioCue('calcKey');">
  584. </div>
  585. <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;">
  586. <span style="font-weight: bold; font-size:13px; color:#aaa;" data-i18n="calcSpeedLabel">Скорость (м/с):</span>
  587. <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');">
  588. <select id="mediumSelect" class="calc-select" onchange="onSelectMedium(); calculateWaveLength(); playAudioCue('calcKey');"></select>
  589. <span class="hint-icon" style="background:#888;" data-i18n-hint="hintSpeed" data-hint="Выберите среду из списка">?</span>
  590. </div>
  591. </div>
  592. <div class="wave-calculator">
  593. <h3 style="margin-top:0; color:#fff;" data-i18n="calc2Title"><span class="emoji-fix">⏱</span> Калькулятор Периода</h3>
  594. <div class="calc-screen">
  595. <div class="calc-formula">T = 1 / ν | ω = 2πν</div>
  596. <div style="font-size: 13px; color: #81c784; margin-bottom: 5px;" data-i18n="calcCycle">Длительность 1 цикла (Период):</div>
  597. <div class="calc-result"><span id="calcResPeriod">0.00227</span> <span class="calc-s" data-i18n="sec">с</span></div>
  598. </div>
  599. <div class="calc-controls">
  600. <span style="font-weight: bold; font-size: 14px;"><span data-i18n="lblFreqShort">ν (Гц)</span>:</span>
  601. <input type="number" id="calcFreq2" class="calc-input" value="440" min="1" oninput="document.getElementById('calcFreq1').value = this.value; calculateWaveLength(); playAudioCue('calcKey');">
  602. </div>
  603. <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;">
  604. <div id="calcResOmega" data-i18n="omegaLabel">Цикл. частота (ω): ~2764.6 рад/с</div>
  605. </div>
  606. </div>
  607. </div>
  608. <h2 class="content-header gradient-text" style="margin-top:35px;" data-i18n="faqTitle">💬 Динамические вопросы и ответы (FAQ)</h2>
  609. <div id="dynamic-faq-container"></div>
  610. <h3 data-i18n="resTitleRU" style="text-align: center; font-size: 20px; margin-top: 45px; color: var(--accent);">🇷🇺 Российские образовательные платформы</h3>
  611. <div class="resource-grid">
  612. <a href="https://foxford.ru/wiki/fizika" target="_blank" class="premium-link-btn foxford-btn interactive-hover" onclick="playAudioCue('glassBell');">
  613. <span class="emoji-fix" style="font-size:18px;">🦊</span>
  614. <span data-i18n="resFoxford">Фоксфорд.Учебник</span>
  615. </a>
  616. <a href="https://interneturok.ru/subject/physics" target="_blank" class="premium-link-btn ru-btn interactive-hover" onclick="playAudioCue('glassBell');">
  617. <span class="emoji-fix" style="font-size:18px;">📚</span>
  618. <span data-i18n="resInterneturok">InternetUrok: Физика</span>
  619. </a>
  620. <a href="http://www.virtulab.net/" target="_blank" class="premium-link-btn ru-btn interactive-hover" onclick="playAudioCue('glassBell');">
  621. <span class="emoji-fix" style="font-size:18px;">🏫</span>
  622. <span data-i18n="resVirtulab">VirtuLab: Лаборатории</span>
  623. </a>
  624. <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');">
  625. <span class="emoji-fix" style="font-size:18px;">⬛</span>
  626. <span data-i18n="linkRutube">Rutube: Лекции по физике</span>
  627. </a>
  628. <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');">
  629. <span class="emoji-fix" style="font-size:18px;">🟦</span>
  630. <span data-i18n="linkVK">VK Видео: Волны и колебания</span>
  631. </a>
  632. </div>
  633. <h3 data-i18n="resTitleINT" style="text-align: center; font-size: 20px; margin-top: 35px; color: var(--accent);">🌍 Международные симуляторы и материалы</h3>
  634. <div class="resource-grid">
  635. <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');">
  636. <span class="emoji-fix" style="font-size:18px;">🟥</span>
  637. <span data-i18n="linkYT">YouTube: Урок Павла Виктора</span>
  638. </a>
  639. <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');">
  640. <span class="emoji-fix" style="font-size:18px;">🧪</span>
  641. <span data-i18n="resPhet">PhET: Волна на струне</span>
  642. </a>
  643. <a href="https://academo.org/demos/wave-interference-beat-frequency/" target="_blank" class="premium-link-btn sim-btn interactive-hover" onclick="playAudioCue('glassBell');">
  644. <span class="emoji-fix" style="font-size:18px;">📡</span>
  645. <span data-i18n="resAcademo">Academo: Интерференция</span>
  646. </a>
  647. <a href="https://www.falstad.com/fourier/" target="_blank" class="premium-link-btn sim-btn interactive-hover" onclick="playAudioCue('glassBell');">
  648. <span class="emoji-fix" style="font-size:18px;">📈</span>
  649. <span data-i18n="resFalstad">Falstad: Синтезатор</span>
  650. </a>
  651. </div>
  652. </div>
  653. <div class="app-footer">
  654. HarmoniX v21.0 Enterprise Nova | Лаборатория Волновой Физики © 2026<br>
  655. Интерактивный анализ механических волн, Теория Фурье и Фигуры Лиссажу
  656. </div>
  657. </div>
  658. <!-- ТЕСТИРОВАНИЕ: СЛАЙДЕР И УМНЫЙ ТАЙМЕР -->
  659. <div id="test-overlay" class="modal-overlay">
  660. <div class="modal-content glass-panel" style="z-index: 99999;">
  661. <span class="close-btn" onclick="closeTest(); playAudioCue('mainPulse');">&times;</span>
  662. <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>
  663. <div id="test-start-menu">
  664. <p style="text-align:center; font-size: 15px; margin-bottom:15px; font-weight: bold;" data-i18n="testSelectSub">Выберите режим тестирования:</p>
  665. <div class="test-modes" id="testModesContainer">
  666. <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>
  667. <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>
  668. <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>
  669. <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>
  670. </div>
  671. </div>
  672. <div id="quiz-process" style="display: none;">
  673. <div class="quiz-timer-pill glass-panel btn-shine" id="quizTimerBar" style="display:none;">
  674. <div class="timer-progress-bg">
  675. <div id="quizTimerProgress" class="timer-progress-fill"></div>
  676. </div>
  677. <div class="timer-content">
  678. <div style="display:flex; gap: 8px; align-items:center;">
  679. <span class="timer-display" id="quizTimerVal">00:00</span>
  680. </div>
  681. <div style="display:flex; gap: 6px;">
  682. <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>
  683. <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>
  684. </div>
  685. </div>
  686. </div>
  687. <div class="question-card-wrapper" id="quiz-card-wrapper">
  688. <div id="quiz-container"></div>
  689. </div>
  690. <div class="quiz-nav-row" id="quiz-nav-controls">
  691. <button class="quiz-nav-btn interactive-hover" id="btnPrevQ" onclick="prevQuestion(); playAudioCue('mainPulse');" disabled>
  692. ◀ <span data-i18n="btnPrev">Назад</span>
  693. </button>
  694. <span id="quiz-step-indicator" style="font-weight: 800; font-size: 15px; color: var(--accent);">1 / 5</span>
  695. <button class="quiz-nav-btn interactive-hover" id="btnNextQ" onclick="nextQuestion(); playAudioCue('mainPulse');">
  696. <span data-i18n="btnNext">Вперед</span> ▶
  697. </button>
  698. </div>
  699. <div id="test-result-screen">
  700. <div class="score-circle-wrapper">
  701. <svg width="140" height="140" viewBox="0 0 120 120" class="circular-progress">
  702. <circle class="bg" cx="60" cy="60" r="54"></circle>
  703. <circle class="progress" id="scoreProgress" cx="60" cy="60" r="54"></circle>
  704. </svg>
  705. <div class="score-text" id="scoreCircleText">0%</div>
  706. </div>
  707. <div id="test-result-text" style="font-size: 20px; font-weight: 900; margin-bottom: 15px;"></div>
  708. <div class="review-box" id="quiz-explanation"></div>
  709. <div class="test-controls" style="margin-top: 25px;">
  710. <button class="btn btn-test-special interactive-hover" onclick="openTestMenu(); playAudioCue('glassBell');" style="font-size: 15px; padding: 12px 24px;">
  711. <span class="emoji-fix">🔄</span> <span data-i18n="btnTryAgain">Пройти снова</span>
  712. </button>
  713. <button class="btn btn-reset interactive-hover" onclick="closeTest(); playAudioCue('mainPulse');" style="font-size: 15px; padding: 12px 24px;">
  714. <span class="emoji-fix">🚪</span> <span data-i18n="btnExit">Выйти</span>
  715. </button>
  716. </div>
  717. </div>
  718. </div>
  719. </div>
  720. </div>
  721. <!-- ИНФО ОВЕРЛЕИ -->
  722. <div id="info-overlay" class="modal-overlay">
  723. <div class="modal-content glass-panel" style="z-index: 99999;">
  724. <span class="close-btn" onclick="closeInfoModal(); playAudioCue('mainPulse');">&times;</span>
  725. <div id="modal-formulas" class="info-content" style="display:none;">
  726. <h2 class="gradient-text" style="text-align:center;"><span class="emoji-fix">📚</span> <span data-i18n="modalFormulasTitle">Справочник формул</span></h2>
  727. <div class="formula-grid">
  728. <div class="formula-card pop-animation interactive-hover" data-i18n="form1"><b>Уравнение бегущей волны:</b><br> y(x,t) = A · cos(w·t - k·x + θ)</div>
  729. <div class="formula-card pop-animation interactive-hover" data-i18n="form2"><b>Гармоническое колебание:</b><br> y(t) = A · cos(w·t + θ)</div>
  730. <div class="formula-card pop-animation interactive-hover" data-i18n="form3"><b>Период и частота:</b><br> T = 1 / V</div>
  731. <div class="formula-card pop-animation interactive-hover" data-i18n="form4"><b>Циклическая частота:</b><br> w = 2 · π · V</div>
  732. <div class="formula-card pop-animation interactive-hover" data-i18n="form5"><b>Длина волны:</b><br> λ = v / V = v · T</div>
  733. <div class="formula-card pop-animation interactive-hover" data-i18n="form6"><b>Волновое число:</b><br> k = 2 · π / λ</div>
  734. <div class="formula-card pop-animation interactive-hover" data-i18n="form7"><b>Эффект Доплера:</b><br> f' = f · (v ± v₀) / (v ∓ vₛ)</div>
  735. <div class="formula-card pop-animation interactive-hover" data-i18n="form8"><b>Акустический импеданс:</b><br> Z = ρ · v</div>
  736. </div>
  737. <h3 style="text-align:center; color:var(--accent); margin-top:25px;" data-i18n="fConst">⚛️ Фундаментальные константы</h3>
  738. <div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap:12px; margin-top:10px;">
  739. <div class="formula-card pop-animation interactive-hover" style="text-align:center; padding: 14px;" onclick="playAudioCue('calcKey')">
  740. <b data-i18n="c1">Скорость света (c)</b>
  741. <span style="color:#00E676; font-size:18px;">3·10⁸ м/с</span>
  742. </div>
  743. <div class="formula-card pop-animation interactive-hover" style="text-align:center; padding: 14px;" onclick="playAudioCue('calcKey')">
  744. <b data-i18n="c2">Скорость звука (v)</b>
  745. <span style="color:#00E676; font-size:18px;">~343 м/с</span>
  746. </div>
  747. <div class="formula-card pop-animation interactive-hover" style="text-align:center; padding: 14px;" onclick="playAudioCue('calcKey')">
  748. <b data-i18n="c3">Число Пи (π)</b>
  749. <span style="color:#00E676; font-size:18px;">3.14159...</span>
  750. </div>
  751. </div>
  752. <div style="margin-top:20px; text-align:center;">
  753. <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>
  754. </div>
  755. </div>
  756. <!-- ОБНОВЛЕННЫЙ ЖУРНАЛ ИЗМЕНЕНИЙ И ЗВЕЗДЫ -->
  757. <div id="modal-about" class="info-content" style="display:none;">
  758. <h2 class="gradient-text" style="text-align:center;"><span class="emoji-fix">🚀</span> <span data-i18n="modalAboutTitle">О платформе HarmoniX</span></h2>
  759. <!-- РЕЙТИНГ ОБНОВЛЕНИЯ -->
  760. <div class="about-section pop-animation" style="text-align: center; border-color: var(--accent); background: rgba(var(--panel-rgb), 0.95); box-shadow: 0 4px 20px var(--btn-glow);">
  761. <h3 style="justify-content: center;"><span class="emoji-fix">🌟</span> Оцените обновление HarmoniX</h3>
  762. <p style="font-size: 13px; opacity: 0.8; margin-bottom: 5px;">Ваша оценка поможет сделать приложение еще лучше!</p>
  763. <div class="star-rating">
  764. <input type="radio" id="star5" name="rating" value="5" onclick="rateApp(5)"><label for="star5">★</label>
  765. <input type="radio" id="star4" name="rating" value="4" onclick="rateApp(4)"><label for="star4">★</label>
  766. <input type="radio" id="star3" name="rating" value="3" onclick="rateApp(3)"><label for="star3">★</label>
  767. <input type="radio" id="star2" name="rating" value="2" onclick="rateApp(2)"><label for="star2">★</label>
  768. <input type="radio" id="star1" name="rating" value="1" onclick="rateApp(1)"><label for="star1">★</label>
  769. </div>
  770. </div>
  771. <div class="about-section pop-animation interactive-hover">
  772. <h3 data-i18n="aboutDescTitle"><span class="emoji-fix">ℹ️</span> Философия проекта</h3>
  773. <p data-i18n="aboutDescText">HarmoniX — это высокотехнологичная PWA-среда для визуального моделирования волновых процессов, гармонического сложения и анализа спектра Фурье.</p>
  774. </div>
  775. <h3 data-i18n="aboutHistoryTitle" style="color: var(--accent); margin-top:20px;">📜 Журнал изменений (Changelog)</h3>
  776. <div class="timeline">
  777. <div class="timeline-item pop-animation interactive-hover">
  778. <h4>🌟 v21.0 Nova Update (Текущая):</h4>
  779. <ul>
  780. <li>Добавлены смарт-уведомления (Toasts) при входе.</li>
  781. <li>Внедрена система онбординга (анимированные стрелки-подсказки).</li>
  782. <li>Реализована система оценки проекта по звездам (Rating System).</li>
  783. <li>Полностью переработан и расширен Журнал изменений.</li>
  784. </ul>
  785. </div>
  786. <div class="timeline-item pop-animation interactive-hover">
  787. <h4>🔥 v20.0 Enterprise Ultimate:</h4>
  788. <ul>
  789. <li>Максимальная интерактивность (Глобальные Ripple-эффекты).</li>
  790. <li>Аппаратный аудио-клиппинг через <code>WaveShaperNode</code>.</li>
  791. <li>Анимация вспышки фото-затвора при сохранении графика.</li>
  792. <li>Умное сглаживание графиков через `requestAnimationFrame`.</li>
  793. </ul>
  794. </div>
  795. <div class="timeline-item pop-animation interactive-hover">
  796. <h4>👑 v19.0 Enterprise Grand Master:</h4>
  797. <ul>
  798. <li>Интерактивный SVG-круг прогресса тестирования с анимацией Count-up.</li>
  799. <li>"Живые" ссылки на Wikipedia в ответах.</li>
  800. <li>Интерактивный круглый логотип. Новые формулы Доплера.</li>
  801. </ul>
  802. </div>
  803. <div class="timeline-item pop-animation interactive-hover">
  804. <h4>🔒 v18.0 Security Addition:</h4>
  805. <ul>
  806. <li>Интеграция модуля ТЗИ (Акустическая маскировка), генератор белого шума.</li>
  807. <li>100% перевод интерфейса (i18n). Умный селектор на 30 сред.</li>
  808. <li>Волновая форма кнопок (18px 6px).</li>
  809. </ul>
  810. </div>
  811. <div class="timeline-item pop-animation interactive-hover">
  812. <h4>🚀 v1.0 - 16.0 Эволюция:</h4>
  813. <ul>
  814. <li>Двуязычная адаптация, раздельный WebAudio.</li>
  815. <li>Генератор вопросов, Фигуры Лиссажу, базовый синтезатор Фурье.</li>
  816. </ul>
  817. </div>
  818. </div>
  819. </div>
  820. </div>
  821. </div>
  822. <script>
  823. let currentLang = 'ru';
  824. let currentPreset = 'sine';
  825. let waves = [{ f: 440, a: 80, p: 0, type: 'sine' }, { f: 880, a: 0, p: 0, type: 'sine' }, { f: 1320, a: 0, p: 0, type: 'sine' }];
  826. let currentWave = 0, freqStep = 100, time = 0, animSpeedMultiplier = 1.0;
  827. let isClipped = false, isAudioOn = false, isPaused = false;
  828. let isGlassOn = true, isGlowOn = false, isGridOn = true, isLissajous = false, showLabels = true;
  829. let waveCtx = null, uiCtx = null, oscillators = [];
  830. let animationFrameId = null, cachedBaseColor = '#2196F3';
  831. const BASE_ANIM_SPEED = 0.08;
  832. let isMaskingTZI = false, noiseSource = null;
  833. let masterGainNode = null, masterWaveShaper = null;
  834. // --- УВЕДОМЛЕНИЯ (TOASTS) ---
  835. function showToast(title, text, icon, duration = 6000) {
  836. const container = document.getElementById('toast-container');
  837. if(!container) return;
  838. const toast = document.createElement('div');
  839. toast.className = 'toast';
  840. toast.innerHTML = `<div class="toast-icon emoji-fix">${icon}</div><div class="toast-content"><div class="toast-title">${title}</div><div class="toast-text">${text}</div></div>`;
  841. container.appendChild(toast);
  842. void toast.offsetWidth; // Reflow
  843. toast.classList.add('show');
  844. playAudioCue('tick');
  845. toast.onclick = () => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 500); playAudioCue('click'); };
  846. if(duration > 0) {
  847. setTimeout(() => {
  848. if(toast.parentElement) { toast.classList.remove('show'); setTimeout(() => toast.remove(), 500); }
  849. }, duration);
  850. }
  851. }
  852. // --- СТРЕЛОЧКИ ПОДСКАЗОК (ОНБОРДИНГ) ---
  853. function showOnboardingPointers() {
  854. function attach(targetId, text) {
  855. let target = document.getElementById(targetId);
  856. if(target && target.style.display !== 'none') {
  857. let hint = document.createElement('div');
  858. hint.className = `onboarding-hint`;
  859. hint.innerHTML = `<div class="onboarding-text">${text}</div><span class="emoji-fix" style="font-size:28px;">⬇️</span>`;
  860. hint.style.top = '-48px'; hint.style.left = '50%'; hint.style.transform = 'translateX(-50%)';
  861. target.appendChild(hint);
  862. target.addEventListener('click', () => { if(hint.parentElement) hint.remove(); }, {once:true});
  863. setTimeout(() => { if(hint.parentElement) hint.remove(); }, 20000); // Автоудаление
  864. }
  865. }
  866. setTimeout(() => attach('btnMainSnap', 'Новая анимация!'), 2000);
  867. setTimeout(() => attach('btnTziMode', 'Генератор шума'), 4000);
  868. setTimeout(() => attach('btnMainTestMode', 'Геймификация!'), 6000);
  869. }
  870. // --- РЕЙТИНГ ---
  871. function rateApp(stars) {
  872. let msg = currentLang === 'ru' ? `Спасибо за оценку! Вы поставили ${stars} звезд.` : `Thanks for rating! You gave ${stars} stars.`;
  873. showToast("⭐⭐⭐⭐⭐", msg, "🎉", 4000);
  874. playAudioCue('glassBell');
  875. }
  876. // --- ГЛОБАЛЬНЫЙ RIPPLE ЭФФЕКТ ДЛЯ МАКСИМАЛЬНОГО ИНТЕРАКТИВА ---
  877. document.addEventListener('click', function(e) {
  878. if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.classList.contains('toast')) return;
  879. const ripple = document.createElement('div');
  880. ripple.className = 'global-ripple';
  881. ripple.style.left = e.clientX + 'px';
  882. ripple.style.top = e.clientY + 'px';
  883. document.body.appendChild(ripple);
  884. setTimeout(() => ripple.remove(), 600);
  885. });
  886. const i18n = {
  887. ru: {
  888. appTitle: "HarmoniX", appSubtitle: "Интерактивная Лаборатория Волновой Физики", btnStart: "Начать работу",
  889. menuSettings: "Настройки", lblGraphTools: "Инструменты графиков", lblLabels: "Подписи",
  890. lblNeon: "Неон", lblGrid: "Сетка", lblLissajous: "Лиссажу", lblGlass: "Жидкое стекло",
  891. lblPresets: "Пресеты", btnExport: "Экспорт", btnImport: "Импорт", lblTheme: "Оформление",
  892. lblThemeSelect: "10 ТЕМ ОФОРМЛЕНИЯ:", lblCustomBg: "Свои обои:", lblBgFilter: "Фильтр фона:",
  893. btnDelBg: "Сбросить фон", btnMenu: "Меню", btnReload: "Обновить", btnTest: "Тесты", btnFull: "На весь экран", btnExitFull: "Выйти",
  894. pnl1: "1. Базовое колебание", pnl2: "2. Панель Управления", pnl3: "3. Теорема Фурье (Сумма)", pnl4: "4. Спектрограмма",
  895. lblStep: "Шаг частоты:", lblFreq: "Частота (Гц):", lblAmp: "Амплитуда (%):", lblPhase: "Сдвиг фазы (°):", lblSpeed: "Скорость волны:",
  896. btnAudio: "Динамик", btnMute: "Выкл. Звук", btnPause: "Стоп-кадр", btnPlay: "Запуск", btnClip: "Нелинейн. преобр.", btnUnclip: "Выкл. преобр.", btnReset: "Сброс",
  897. btnSine: "Синус", btnSquare: "Меандр", btnTriangle: "Треуг.", btnSaw: "Пила", btnSave: "Сохранить", btnSaveCompact: "Снимок",
  898. eduHeader: "📚 Академия Волновой Механики", eduCard1Title: "〰️ Природа механических волн",
  899. eduCard1Text: "Механическая волна представляет собой процесс распространения колебательного возмущения в упругой среде. При этом частицы среды лишь совершают малые колебания возле равновесия, а переносится исключительно <b>ЭНЕРГИЯ И ИМПУЛЬС</b>.",
  900. eduCard1Params: "Ключевые физические параметры:", eduParam1: "• <b>Период (T):</b> длительность одного полного колебания.", eduParam2: "• <b>Частота (V):</b> число полных циклов за 1 секунду.", eduParam3: "• <b>Циклическая частота (w):</b> угловая скорость изменения фазы.", eduParam4: "• <b>Сдвиг фазы (θ):</b> начальное положение фазового угла.",
  901. eduCard2Title: "📉 Теорема и спектральный анализ Фурье", eduCard2Text1: "В 1822 году Жан-Батист Фурье доказал: <b>ЛЮБОЙ ПЕРИОДИЧЕСКИЙ СИГНАЛ</b> сложной формы может быть разложен в ряд простых гармонических волн кратных частот.",
  902. eduCard2Text2: "Это фундаментальная база цифровой обработки сигналов: от синтезаторов звука и сжатия MP3 до радиопередачи Wi-Fi и сетей 5G.",
  903. calcTitle: "🧮 Калькулятор длины волны", calc2Title: "⏱ Калькулятор Периода", calcCustomMedium: "Свой вариант...",
  904. faqTitle: "💬 Динамические вопросы и ответы (FAQ)",
  905. resTitleRU: "🇷🇺 Российские образовательные платформы",
  906. resTitleINT: "🌍 Международные симуляторы и материалы",
  907. resPhet: "PhET: Волна на струне", resAcademo: "Academo: Интерференция", resFalstad: "Falstad: Синтезатор",
  908. resVirtulab: "VirtuLab: Лаборатории", resInterneturok: "InternetUrok: Физика", resFoxford: "Фоксфорд.Учебник",
  909. testHeader: "Тестирование знаний", testSelectSub: "Выберите режим тестирования:", btnCheck: "Сдать",
  910. modeExp: "Экспресс", time5: "5 (Без таймера)", modeStd: "Стандарт", time15: "15 (5 мин)", modeFull: "Полный курс", time30: "30 (10 мин)", modeUltra: "Ультра Профи", time50: "50 (15 мин)", btnExit: "Выйти", btnTryAgain: "Пройти снова",
  911. telWave: "Волна", telF: "Частота", telA: "Амплитуда", telClip: "Нелинейное", telXAxis: "Ось X", clipOn: "Вкл", clipOff: "Выкл",
  912. lblTelPhase: "Фаза", telY: "Смещение", telSumY: "Σy", telPeaks: "Пики", lblTimeLeft: "Осталось",
  913. navHeader: "Разделы и Инфо", navMain: "Главная панель", navTest: "Итоговое Тестирование", navFormulas: "Справочник формул", navAbout: "О проекте HarmoniX",
  914. lblHeightResize: "Высота холстов графиков:", modalFormulasTitle: "Справочник формул", modalAboutTitle: "О платформе HarmoniX",
  915. modalExtLinks: "🎥 Рекомендованные видеоматериалы:", linkRutube: "Rutube: Лекции по физике", linkVK: "VK Видео: Волны и колебания", linkYT: "YouTube: Лекция Павла Виктора", linkWiki1: "Ряд Фурье (Википедия)",
  916. aboutDescTitle: "ℹ️ Философия проекта", aboutDescText: "HarmoniX — интерактивная вычислительная лаборатория для визуального моделирования волновых процессов, гармонического сложения и анализа спектра Фурье.",
  917. aboutHistoryTitle: "📜 Журнал изменений (Changelog)",
  918. form1: "<b>Уравнение бегущей волны:</b><br> y(x,t) = A · cos(w·t - k·x + θ)", form2: "<b>Гармоническое колебание:</b><br> y(t) = A · cos(w·t + θ)",
  919. form3: "<b>Период и частота:</b><br> T = 1 / V", form4: "<b>Циклическая частота:</b><br> w = 2 · π · V", form5: "<b>Длина волны:</b><br> λ = v / V = v · T",
  920. form6: "<b>Волновое число:</b><br> k = 2 · π / λ", form7: "<b>Эффект Доплера:</b><br> f' = f · (v ± v₀) / (v ∓ vₛ)", form8: "<b>Акустический импеданс:</b><br> Z = ρ · v",
  921. themes: { default: "⚪ Светлая (Базовая)", dark: "⚫ Темная (Ночная)", neon: "🟣 Неон (Киберпанк)", ocean: "🌊 Океан (Глубокая)", forest: "🌲 Лес (Природная)", sunset: "🌅 Закат (Теплая)", cyber: "🟡 Кибер (Желтая)", coffee: "☕ Латте (Кофе)", space: "🌌 Глубокий Космос", sakura: "🌸 Сакура (Розовая)" },
  922. filters: { none: "🚫 Без фильтра", "blur(14px) saturate(140%)": "🌫 Глубокое размытие", "invert(100%)": "💡 Негатив / Инверсия", "contrast(150%) brightness(85%)": "🎬 Кинематограф", "sepia(100%)": "📜 Сепия (Винтаж)" },
  923. hz: "Гц", meters: "м", sec: "с",
  924. tziTitle: "ТЗИ: Виброакустическая защита",
  925. tziDesc: "Активация генератора шума для маскировки речевой информации от средств перехвата:",
  926. btnTziStart: "Активировать маскировку", btnTziStop: "Отключить маскировку",
  927. calcAirDesc: "Среда: Воздух (20°C)", calcSpeedLabel: "Скорость (м/с):", calcCycle: "Длительность 1 цикла (Период):",
  928. hintYellow: "Ищите кружки [?] для подсказок!", btnPickImg: "📂 Выбрать картинку", btnDelBg: "Сбросить фон",
  929. testTimeUp: "⏰ Время вышло! Ответы проверены автоматически.",
  930. testReviewTitle: "Детальный разбор вопросов:", testSkipped: "Пропущен",
  931. testResTitle: "Итоговый результат:", testResOf: "из",
  932. testRes90: "🎉 Превосходно! Уровень эксперта.", testRes60: "👍 Хорошо, курс освоен.", testResLow: "😅 Рекомендуем повторить теорию на сайте.",
  933. omegaLabel: "Цикл. частота (ω):", radS: "рад/с", lblFreqShort: "ν (Гц)", lblPhaseShort: "Фаза",
  934. btnPrev: "Назад", btnNext: "Вперед", qOf: "из", qWord: "Вопрос", wikiLink: "Читать подробнее в Wikipedia", hyperPhysics: "🔗 База знаний HyperPhysics (GSU)",
  935. fConst: "⚛️ Фундаментальные константы", c1: "Скорость света (c)", c2: "Скорость звука (v)", c3: "Число Пи (π)",
  936. telTHD: "THD (Искажения)", telLimit: "Порог среза", toastWelcome: "👋 Добро пожаловать!", toastWelcomeTxt: "HarmoniX v21.0 успешно запущен. Закройте меня кликом.", toastUpdate: "🚀 Что нового?", toastUpdateTxt: "Добавлены смарт-подсказки, система рейтингов и Ripple-анимации.", toastSec: "🛡️ Модуль ТЗИ", toastSecTxt: "Обязательно попробуйте генератор шума внизу страницы!"
  937. },
  938. en: {
  939. appTitle: "HarmoniX", appSubtitle: "Interactive Wave Physics Laboratory", btnStart: "Start Analysis",
  940. menuSettings: "Settings", lblGraphTools: "Graph Tools", lblLabels: "Labels",
  941. lblNeon: "Neon Glow", lblGrid: "Grid", lblLissajous: "Lissajous", lblGlass: "Liquid Glass",
  942. lblPresets: "Presets", btnExport: "Export", btnImport: "Import", lblTheme: "Themes",
  943. lblThemeSelect: "10 COLOR THEMES:", lblCustomBg: "Custom Wallpaper:", lblBgFilter: "Background Filter:",
  944. btnDelBg: "Reset Wallpaper", btnMenu: "Menu", btnReload: "Reload", btnTest: "Quizzes", btnFull: "Fullscreen", btnExitFull: "Exit",
  945. pnl1: "1. Basic Oscillation", pnl2: "2. Control Panel", pnl3: "3. Fourier Theorem (Sum)", pnl4: "4. Spectrogram",
  946. lblStep: "Frequency Step:", lblFreq: "Frequency (Hz):", lblAmp: "Amplitude (%):", lblPhase: "Phase Shift (°):", lblSpeed: "Wave Speed:",
  947. btnAudio: "Audio", btnMute: "Mute", btnPause: "Pause", btnPlay: "Play", btnClip: "Non-linear Dist.", btnUnclip: "Disable", btnReset: "Reset",
  948. btnSine: "Sine", btnSquare: "Square", btnTriangle: "Triangle", btnSaw: "Sawtooth", btnSave: "Save", btnSaveCompact: "Snap",
  949. eduHeader: "📚 Wave Mechanics Academy", eduCard1Title: "〰 Nature of Mechanical Waves",
  950. eduCard1Text: "A mechanical wave propagates oscillatory disturbances in an elastic medium, transferring energy without mass transfer.",
  951. 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.",
  952. eduCard2Title: "📉 Fourier Theorem & Analysis", eduCard2Text1: "Any complex periodic signal can be represented as an exact sum of harmonic sine waves.",
  953. eduCard2Text2: "This is the basis of all digital signal processing: from audio equalization to data packet transmission over Wi-Fi and 5G cellular networks.",
  954. calcTitle: "🧮 Wavelength Calculator", calc2Title: "⏱ Period Calculator", calcCustomMedium: "Custom...",
  955. faqTitle: "💬 Dynamic Questions and Answers (FAQ)",
  956. resTitleRU: "🇷🇺 Russian Educational Platforms",
  957. resTitleINT: "🌍 International Simulators",
  958. resPhet: "PhET: Wave on a String", resAcademo: "Academo: Wave Interference", resFalstad: "Falstad: Synth",
  959. resVirtulab: "VirtuLab: Science Labs", resInterneturok: "InternetUrok: Physics", resFoxford: "Foxford Tutorials",
  960. testHeader: "Knowledge Testing", testSelectSub: "Select test mode:", btnCheck: "Submit",
  961. 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",
  962. telWave: "Wave", telF: "Frequency", telA: "Amplitude", telClip: "Non-linear", telXAxis: "X Axis", clipOn: "ON", clipOff: "OFF",
  963. lblTelPhase: "Phase", telY: "Displacement", telSumY: "Σy", telPeaks: "Peaks", lblTimeLeft: "Time left",
  964. navHeader: "Sections & Info", navMain: "Dashboard", navTest: "Testing Quiz", navFormulas: "Formulas Reference", navAbout: "About HarmoniX",
  965. lblHeightResize: "Canvas Height:", modalFormulasTitle: "Formulas Reference", modalAboutTitle: "About HarmoniX Platform",
  966. modalExtLinks: "🎥 Recommended Videos:", linkRutube: "Rutube: Wave Physics Lectures", linkVK: "VK Video: Waves & Oscillations", linkYT: "YouTube: Pavel Victor Lesson", linkWiki1: "Fourier Series (Wikipedia)",
  967. aboutDescTitle: "ℹ️ Philosophy of the Project", aboutDescText: "HarmoniX is an interactive simulation suite for visual wave physics, superposition, and Fourier analysis.",
  968. aboutHistoryTitle: "📜 Changelog",
  969. 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 + θ)",
  970. 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",
  971. 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",
  972. 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)" },
  973. filters: { none: "🚫 No Filter", "blur(14px) saturate(140%)": "🌫 Deep Blur", "invert(100%)": "💡 Invert / Negative", "contrast(150%) brightness(85%)": "🎬 Cinematic", "sepia(100%)": "📜 Vintage Sepia" },
  974. hz: "Hz", meters: "m", sec: "s",
  975. tziTitle: "InfoSec: Acoustic Protection",
  976. tziDesc: "Activation of white noise generator for speech masking against interception:",
  977. btnTziStart: "Activate Masking", btnTziStop: "Disable Masking",
  978. calcAirDesc: "Medium: Air (20°C)", calcSpeedLabel: "Speed (m/s):", calcCycle: "1-Cycle Duration (Period):",
  979. hintYellow: "Look for yellow [?] circles for hints!", btnPickImg: "📂 Choose Picture", btnDelBg: "Reset Wallpaper",
  980. testTimeUp: "⏰ Time is up! Answers were checked automatically.",
  981. testReviewTitle: "Detailed Questions Review:", testSkipped: "Skipped",
  982. testResTitle: "Final Result:", testResOf: "out of",
  983. testRes90: "🎉 Excellent! Expert level.", testRes60: "👍 Good, solid knowledge.", testResLow: "😅 We recommend reviewing the theory on the site.",
  984. omegaLabel: "Angular Freq (ω):", radS: "rad/s", lblFreqShort: "ν (Hz)", lblPhaseShort: "Phase",
  985. btnPrev: "Back", btnNext: "Next", qOf: "of", qWord: "Question", wikiLink: "Read more on Wikipedia", hyperPhysics: "🔗 HyperPhysics Database (GSU)",
  986. fConst: "⚛️ Fundamental constants", c1: "Speed of light (c)", c2: "Speed of sound (v)", c3: "Pi Number (π)",
  987. telTHD: "THD (Distortion)", telLimit: "Clip Limit", toastWelcome: "👋 Welcome!", toastWelcomeTxt: "HarmoniX v21.0 is online.", toastUpdate: "🚀 What's New?", toastUpdateTxt: "Added smart toasts, star rating and new pointer animations.", toastSec: "🛡️ InfoSec Module", toastSecTxt: "Try the acoustic noise generator below!"
  988. }
  989. };
  990. const globalFaqData = {
  991. ru: [
  992. { 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>" },
  993. { q: "Как фазовый сдвиг влияет на интерференцию?", a: "При сдвиге 0° волны <b>синфазны</b> и возникает <span class='key-term'>конструктивная интерференция</span> (усиление). Если сдвиг 180° (противофаза) — волны взаимно гасятся. Этот эффект используется в системах активного шумоподавления (ANC)." },
  994. { q: "Что происходит со спектром при нелинейном ограничении (клиппинге)?", a: "При клиппинге верхушки синусоиды срезаются, приближая сигнал к прямоугольной форме (меандру). Это рождает <span class='key-term'>бесконечное число нечетных паразитных гармоник</span>, которые ярко видны в спектрограмме." },
  995. { q: "Что показывает теорема Фурье?", a: "Она доказывает, что любой сложный, но периодический сигнал можно точно собрать (синтезировать) из набора <b>простых бесконечных синусоид</b> разных частот и амплитуд." },
  996. { q: "От чего зависит скорость волны?", a: "Скорость механической волны зависит исключительно от свойств среды (ее плотности, упругости, температуры). В воздухе это ~340 м/с, в воде ~1480 м/с, в стали почти <b>6000 м/с</b>!" },
  997. { q: "Что такое Акустическая маскировка (ТЗИ)?", a: "<b>Техническая защита информации (ТЗИ)</b> в акустике включает генерацию виброакустического белого шума, маскирующего речевую информацию от подслушивания через окна, стены и вентиляцию." },
  998. { q: "Почему звуковая волна не переносит вещество?", a: "Потому что частицы среды совершают лишь малые колебательные движения возле своего положения равновесия, сталкиваясь с соседями и передавая им только энергию (импульс)." },
  999. { q: "В чем разница между продольной и поперечной волной?", a: "В поперечной волне частицы колеблются <b>перпендикулярно</b> направлению движения волны (волна на струне). В продольной — <b>вдоль</b> направления волны (звук в воздухе)." }
  1000. ],
  1001. en: [
  1002. { 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>" },
  1003. { 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)." },
  1004. { 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>." },
  1005. { 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>." },
  1006. { 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." },
  1007. { 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." },
  1008. { q: "Why don't sound waves transport mass?", a: "Particles oscillate purely locally around equilibrium points, transferring momentum elastically." },
  1009. { q: "Transverse vs. Longitudinal waves?", a: "In transverse waves, particle displacement is perpendicular to wave propagation. In longitudinal waves, it's parallel." }
  1010. ]
  1011. };
  1012. const mediaList = [
  1013. { ru: "Воздух (20°C)", en: "Air (20°C)", v: 343 }, { ru: "Воздух (0°C)", en: "Air (0°C)", v: 331 },
  1014. { ru: "Гелий", en: "Helium", v: 965 }, { ru: "Водород", en: "Hydrogen", v: 1284 },
  1015. { ru: "Углекислый газ", en: "Carbon Dioxide", v: 259 }, { ru: "Вода (Пресная)", en: "Fresh Water", v: 1480 },
  1016. { ru: "Морская вода", en: "Sea Water", v: 1530 }, { ru: "Керосин", en: "Kerosene", v: 1324 },
  1017. { ru: "Ртуть", en: "Mercury", v: 1450 }, { ru: "Глицерин", en: "Glycerol", v: 1904 },
  1018. { ru: "Свинец", en: "Lead", v: 2160 }, { ru: "Золото", en: "Gold", v: 3240 },
  1019. { ru: "Серебро", en: "Silver", v: 2680 }, { ru: "Лед", en: "Ice", v: 3980 },
  1020. { ru: "Дерево (Сосна)", en: "Pine Wood", v: 3320 }, { ru: "Дерево (Дуб)", en: "Oak Wood", v: 4000 },
  1021. { ru: "Кирпич", en: "Brick", v: 4180 }, { ru: "Бетон", en: "Concrete", v: 4250 },
  1022. { ru: "Медь", en: "Copper", v: 4700 }, { ru: "Стекло (Оконное)", en: "Window Glass", v: 5640 },
  1023. { ru: "Стекло (Кварцевое)", en: "Quartz Glass", v: 5370 }, { ru: "Сталь", en: "Steel", v: 5900 },
  1024. { ru: "Чугун", en: "Cast Iron", v: 4480 }, { ru: "Железо", en: "Iron", v: 5950 },
  1025. { ru: "Алюминий", en: "Aluminum", v: 6420 }, { ru: "Титановый сплав", en: "Titanium Alloy", v: 6070 },
  1026. { ru: "Бериллий", en: "Beryllium", v: 12890 }, { ru: "Резина", en: "Rubber", v: 150 },
  1027. { ru: "Пробка", en: "Cork", v: 500 }, { ru: "Алмаз", en: "Diamond", v: 12000 }
  1028. ];
  1029. function toggleTZI() {
  1030. isMaskingTZI = !isMaskingTZI;
  1031. let btn = document.getElementById('btnTziMode');
  1032. if (btn) {
  1033. if (isMaskingTZI) {
  1034. btn.innerHTML = `<span class="emoji-fix">🔓</span> <span data-i18n="btnTziStop">${i18n[currentLang].btnTziStop}</span>`;
  1035. btn.style.background = '#F44336'; btn.style.boxShadow = '0 0 15px rgba(244,67,54,0.5)';
  1036. startNoise(); playAudioCue('warn');
  1037. } else {
  1038. btn.innerHTML = `<span class="emoji-fix">🔒</span> <span data-i18n="btnTziStart">${i18n[currentLang].btnTziStart}</span>`;
  1039. btn.style.background = 'var(--tzi-color)'; btn.style.boxShadow = '0 0 15px var(--tzi-glow)';
  1040. stopNoise(); playAudioCue('mainPulse');
  1041. }
  1042. }
  1043. }
  1044. function startNoise() {
  1045. if (!waveCtx) initAudio(); if (!waveCtx) return;
  1046. const bufferSize = waveCtx.sampleRate * 2; const buffer = waveCtx.createBuffer(1, bufferSize, waveCtx.sampleRate); const data = buffer.getChannelData(0);
  1047. for (let i = 0; i < bufferSize; i++) { data[i] = Math.random() * 2 - 1; }
  1048. noiseSource = waveCtx.createBufferSource(); noiseSource.buffer = buffer; noiseSource.loop = true;
  1049. let noiseGain = waveCtx.createGain(); noiseGain.gain.value = 0.08;
  1050. noiseSource.connect(noiseGain); noiseGain.connect(waveCtx.destination); noiseSource.start();
  1051. }
  1052. function stopNoise() { if (noiseSource) { try { noiseSource.stop(); noiseSource.disconnect(); } catch(e){} noiseSource = null; } }
  1053. function makeDistortionCurve(amount) {
  1054. let n_samples = 44100;
  1055. let curve = new Float32Array(n_samples);
  1056. for (let i = 0; i < n_samples; ++i) {
  1057. let x = i * 2 / n_samples - 1;
  1058. curve[i] = x < -0.5 ? -0.5 : (x > 0.5 ? 0.5 : x);
  1059. }
  1060. return curve;
  1061. }
  1062. function initAudio() {
  1063. if (!uiCtx) uiCtx = new (window.AudioContext || window.webkitAudioContext)();
  1064. if (uiCtx.state === 'suspended') uiCtx.resume();
  1065. if (!waveCtx) {
  1066. waveCtx = new (window.AudioContext || window.webkitAudioContext)();
  1067. masterGainNode = waveCtx.createGain();
  1068. masterWaveShaper = waveCtx.createWaveShaper();
  1069. masterWaveShaper.curve = null;
  1070. masterWaveShaper.oversample = '4x';
  1071. masterGainNode.connect(masterWaveShaper);
  1072. masterWaveShaper.connect(waveCtx.destination);
  1073. for (let i = 0; i < 3; i++) {
  1074. let osc = waveCtx.createOscillator(); let gain = waveCtx.createGain();
  1075. osc.type = 'sine'; osc.frequency.setValueAtTime(waves[i].f, waveCtx.currentTime);
  1076. gain.gain.setValueAtTime(0, waveCtx.currentTime); osc.connect(gain);
  1077. gain.connect(masterGainNode); osc.start(); oscillators.push({ osc: osc, gain: gain });
  1078. }
  1079. }
  1080. if (waveCtx.state === 'suspended') waveCtx.resume();
  1081. }
  1082. function startApp() {
  1083. try { initAudio(); } catch(e){}
  1084. let ws = document.getElementById('welcome-screen');
  1085. let mainNav = document.getElementById('main-nav-bar');
  1086. let mainApp = document.getElementById('main-app');
  1087. if(ws) { ws.style.opacity = '0'; ws.style.pointerEvents = 'none'; }
  1088. setTimeout(() => {
  1089. if(ws) ws.style.display = 'none';
  1090. if(mainNav) mainNav.style.display = 'flex';
  1091. if(mainApp) mainApp.style.display = 'flex';
  1092. try { buildSelectOptions(); } catch(e){}
  1093. try { setLang('ru'); } catch(e){}
  1094. try { setPreset('sine'); } catch(e){}
  1095. setTimeout(() => {
  1096. try { resizeAllCanvases(); } catch(e){}
  1097. if (!animationFrameId) { draw(); }
  1098. try { populateMediumSelect(currentLang); } catch(e){}
  1099. try { calculateWaveLength(); } catch(e){}
  1100. try { playAudioCue('glassBell'); } catch(e){}
  1101. // ЗАПУСК СИСТЕМЫ УВЕДОМЛЕНИЙ ПРИ СТАРТЕ
  1102. setTimeout(() => showToast(i18n[currentLang].toastWelcome, i18n[currentLang].toastWelcomeTxt, "✨"), 1000);
  1103. setTimeout(() => showToast(i18n[currentLang].toastUpdate, i18n[currentLang].toastUpdateTxt, "🔥"), 4500);
  1104. setTimeout(() => showToast(i18n[currentLang].toastSec, i18n[currentLang].toastSecTxt, "💡"), 9000);
  1105. showOnboardingPointers();
  1106. }, 50);
  1107. }, 300);
  1108. }
  1109. function playAudioCue(type, param = 0) {
  1110. try {
  1111. if (!uiCtx) return;
  1112. const now = uiCtx.currentTime; const osc = uiCtx.createOscillator(); const gain = uiCtx.createGain();
  1113. osc.connect(gain); gain.connect(uiCtx.destination);
  1114. 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); }
  1115. 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); }
  1116. 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); }
  1117. 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); }
  1118. 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); }
  1119. 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); }
  1120. 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); }
  1121. 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); }
  1122. 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); }
  1123. } catch(e) {}
  1124. }
  1125. window.playSfx = playAudioCue;
  1126. document.addEventListener('mouseover', e => {
  1127. if (e.target.classList && e.target.classList.contains('hint-icon')) {
  1128. const tt = document.getElementById('global-tooltip'); if(!tt) return;
  1129. let hintKey = e.target.getAttribute('data-i18n-hint');
  1130. if(hintKey && i18n[currentLang][hintKey]) { tt.innerHTML = i18n[currentLang][hintKey]; }
  1131. else { tt.innerHTML = e.target.getAttribute('data-hint'); }
  1132. tt.style.opacity = 1; const rect = e.target.getBoundingClientRect();
  1133. let x = rect.left + rect.width / 2 - tt.offsetWidth / 2; let y = rect.top - tt.offsetHeight - 12;
  1134. if (x < 10) x = 10; if (x + tt.offsetWidth > window.innerWidth - 10) x = window.innerWidth - tt.offsetWidth - 10;
  1135. if (y < 10) y = rect.bottom + 12; tt.style.transform = `translate(${x}px, ${y}px)`; playAudioCue('hover');
  1136. }
  1137. }, true);
  1138. document.addEventListener('mouseout', e => {
  1139. if (e.target.classList && e.target.classList.contains('hint-icon')) { const tt = document.getElementById('global-tooltip'); if(tt) tt.style.opacity = 0; }
  1140. }, true);
  1141. function buildSelectOptions() {
  1142. const curTheme = document.documentElement.getAttribute('data-theme') || 'default';
  1143. const themeSel = document.getElementById('themeSelect');
  1144. 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); } }
  1145. const filterSel = document.getElementById('bgFilter');
  1146. 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); } }
  1147. }
  1148. function safeSetText(id, text) { let el = document.getElementById(id); if (el) el.innerText = text; }
  1149. function renderDynamicFAQ(lang) {
  1150. const container = document.getElementById('dynamic-faq-container'); if(!container) return; container.innerHTML = '';
  1151. let pool = globalFaqData[lang] || globalFaqData['ru']; let shuffled = pool.sort(() => 0.5 - Math.random()).slice(0, 4);
  1152. shuffled.forEach(item => {
  1153. let btn = document.createElement('button'); btn.className = 'accordion-btn glass-element interactive-hover btn-shine'; btn.onclick = function() { toggleAccordion(this); playAudioCue('menuClick'); };
  1154. btn.innerHTML = `<span>${item.q}</span> <span>▼</span>`;
  1155. let content = document.createElement('div'); content.className = 'accordion-content'; content.innerHTML = `<p>${item.a}</p>`;
  1156. container.appendChild(btn); container.appendChild(content);
  1157. });
  1158. }
  1159. function populateMediumSelect(lang) {
  1160. const sel = document.getElementById('mediumSelect'); if(!sel) return;
  1161. sel.innerHTML = `<option value="custom" data-i18n="calcCustomMedium">${i18n[lang].calcCustomMedium}</option>`;
  1162. mediaList.forEach(m => { let opt = document.createElement('option'); opt.value = m.v; opt.innerText = `${m[lang] || m.ru} (${m.v})`; sel.appendChild(opt); });
  1163. let inpVal = document.getElementById('calcSpeedCustom').value; let found = Array.from(sel.options).find(o => o.value == inpVal);
  1164. sel.value = found ? found.value : 'custom';
  1165. }
  1166. function setLang(lang) {
  1167. currentLang = lang;
  1168. document.querySelectorAll('.lang-btn').forEach(btn => btn.classList.remove('active'));
  1169. let activeLangBtn = document.querySelector('.lang-' + lang); if(activeLangBtn) activeLangBtn.classList.add('active');
  1170. document.querySelectorAll('[data-i18n]').forEach(el => { const key = el.getAttribute('data-i18n'); if (i18n[lang][key]) el.innerHTML = i18n[lang][key]; });
  1171. document.querySelectorAll('.unit-freq').forEach(el => { el.innerText = i18n[lang].hz; });
  1172. document.querySelectorAll('.calc-m').forEach(el => { el.innerText = i18n[lang].meters || 'м'; });
  1173. document.querySelectorAll('.calc-s').forEach(el => { el.innerText = i18n[lang].sec || 'с'; });
  1174. let btnTZI = document.getElementById('btnTziMode');
  1175. if(btnTZI) { btnTZI.innerHTML = `<span class="emoji-fix">🔒</span> ${isMaskingTZI ? i18n[lang].btnTziStop : i18n[lang].btnTziStart}`; }
  1176. buildSelectOptions(); updateDynamicButtons(); updateTelemetryUI(); populateMediumSelect(lang); calculateWaveLength(); renderDynamicFAQ(lang);
  1177. if (typeof updateQuizStepIndicator === 'function') { updateQuizStepIndicator(); }
  1178. }
  1179. function updateDynamicButtons() {
  1180. const btnTop = document.getElementById('btnTopLabels'); const btnSide = document.getElementById('btnLabelsToggle');
  1181. const txtLab = currentLang === 'ru' ? (showLabels ? 'Подписи: Вкл' : 'Подписи: Выкл') : (showLabels ? 'Labels: On' : 'Labels: Off');
  1182. if (btnTop) btnTop.innerHTML = `<span class="emoji-fix">📊</span> ${showLabels ? (currentLang==='ru'?'Подписи':'Labels') : (currentLang==='ru'?'Без подписей':'No Labels')}`;
  1183. if (btnSide) { btnSide.innerHTML = txtLab; btnSide.classList.toggle('active', showLabels); }
  1184. const btnGlow = document.getElementById('btnGlowToggle');
  1185. if (btnGlow) { btnGlow.innerHTML = currentLang === 'ru' ? (isGlowOn ? 'Неон: Вкл' : 'Неон: Выкл') : (isGlowOn ? 'Neon: On' : 'Neon: Off'); btnGlow.classList.toggle('active', isGlowOn); }
  1186. const btnGrid = document.getElementById('btnGridToggle');
  1187. 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); }
  1188. const btnLis = document.getElementById('btnLissajous');
  1189. 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); }
  1190. let btnAudio = document.getElementById('btnAudio');
  1191. 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}`; }
  1192. let btnPause = document.getElementById('btnPause');
  1193. 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}`; }
  1194. let btnClip = document.getElementById('btnClip');
  1195. if (btnClip) { btnClip.classList.toggle('active', isClipped); btnClip.querySelector('span:last-child').previousElementSibling.innerHTML = isClipped ? i18n[currentLang].btnUnclip : i18n[currentLang].btnClip; }
  1196. }
  1197. 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); }
  1198. function setCanvasHeight(val) { document.querySelectorAll('.canvas-wrapper').forEach(w => { w.style.height = val + 'px'; }); resizeAllCanvases(); }
  1199. 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'); }
  1200. function toggleGlow() { isGlowOn = !isGlowOn; updateDynamicButtons(); playAudioCue('click'); }
  1201. function toggleGrid() { isGridOn = !isGridOn; updateDynamicButtons(); playAudioCue('click'); }
  1202. function toggleLissajous() { isLissajous = !isLissajous; let lm = document.getElementById('lblLisMode'); if (lm) lm.innerText = isLissajous ? "(Lissajous 2D)" : ""; updateDynamicButtons(); playAudioCue('mainPulse'); }
  1203. 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'); }
  1204. function updateGlassIntensity() {
  1205. if (!isGlassOn) {
  1206. document.documentElement.style.setProperty('--glass-alpha', '0.98'); document.documentElement.style.setProperty('--glass-blur', '0px');
  1207. document.documentElement.style.setProperty('--glass-saturate', '100%'); document.documentElement.style.setProperty('--glass-border', 'rgba(204, 204, 204, 0.6)'); return;
  1208. }
  1209. let el = document.getElementById('glassIntensity'); if(!el) return;
  1210. let val = parseInt(el.value); let alpha = 0.95 - (val / 100) * 0.75; let blur = (val / 100) * 30; let saturate = 100 + (val / 100) * 100;
  1211. document.documentElement.style.setProperty('--glass-alpha', alpha.toFixed(2)); document.documentElement.style.setProperty('--glass-blur', blur + 'px');
  1212. document.documentElement.style.setProperty('--glass-saturate', saturate + '%'); document.documentElement.style.setProperty('--glass-border', 'rgba(255, 255, 255, ' + (0.15 + (val/100)*0.35) + ')');
  1213. }
  1214. function autoGlass() {
  1215. const bgLayer = document.getElementById('bg-layer'); if(!bgLayer) return;
  1216. const bgImage = bgLayer.style.backgroundImage; const filterEl = document.getElementById('bgFilter'); const filter = filterEl ? filterEl.value : 'none';
  1217. let idealValue = 30; if (bgImage && bgImage !== 'none') { idealValue = 85; if (filter.includes('blur')) idealValue = 50; }
  1218. let glassEl = document.getElementById('glassIntensity'); if(glassEl) glassEl.value = idealValue;
  1219. setGlassState(true); playAudioCue('mainPulse');
  1220. }
  1221. const canvasResizeObserver = new ResizeObserver(entries => {
  1222. requestAnimationFrame(() => {
  1223. for (let entry of entries) {
  1224. const wrapper = entry.target;
  1225. const canvas = wrapper.querySelector('canvas');
  1226. if (canvas) {
  1227. const w = Math.floor(wrapper.clientWidth);
  1228. const h = Math.floor(wrapper.clientHeight);
  1229. if (w > 0 && h > 0 && (canvas.width !== w || canvas.height !== h)) {
  1230. canvas.width = w;
  1231. canvas.height = h;
  1232. }
  1233. }
  1234. }
  1235. });
  1236. });
  1237. window.addEventListener('DOMContentLoaded', () => {
  1238. document.querySelectorAll('.canvas-wrapper').forEach(wrapper => {
  1239. canvasResizeObserver.observe(wrapper);
  1240. });
  1241. });
  1242. function resizeCanvasElement(canvas) {
  1243. if(!canvas || !canvas.parentElement) return;
  1244. const w = Math.floor(canvas.parentElement.clientWidth); const h = Math.floor(canvas.parentElement.clientHeight);
  1245. if (w === 0 || h === 0) return;
  1246. if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }
  1247. }
  1248. function resizeAllCanvases() { resizeCanvasElement(document.getElementById('canvas-single')); resizeCanvasElement(document.getElementById('canvas-sum')); resizeCanvasElement(document.getElementById('canvas-spectrum')); }
  1249. window.addEventListener('resize', resizeAllCanvases, { passive: true });
  1250. function resetCanvasSizes() {
  1251. document.querySelectorAll('.canvas-wrapper').forEach(w => { w.style.height = '260px'; });
  1252. let el = document.getElementById('canvasHeightSlider'); if(el) el.value = 260;
  1253. resizeAllCanvases(); playAudioCue('mainPulse');
  1254. }
  1255. function updateColorsCache() { cachedBaseColor = getComputedStyle(document.documentElement).getPropertyValue('--base-wave').trim() || '#2196F3'; }
  1256. function toggleFullscreen() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen().catch(err=>{}); } else { if (document.exitFullscreen) document.exitFullscreen(); } }
  1257. document.addEventListener('fullscreenchange', () => { updateDynamicButtons(); });
  1258. 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'; } }
  1259. 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'); }
  1260. function closeInfoModal() { let ov = document.getElementById('info-overlay'); if(!ov) return; ov.classList.remove('show'); setTimeout(()=> { ov.style.display = 'none'; }, 300); playAudioCue('click'); }
  1261. 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'); }
  1262. function onSelectMedium() { const sel = document.getElementById('mediumSelect'); const inp = document.getElementById('calcSpeedCustom'); if (sel.value !== 'custom') { inp.value = sel.value; } }
  1263. 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'; } }
  1264. function calculateWaveLength() {
  1265. let freqEl1 = document.getElementById('calcFreq1'); if(!freqEl1) return;
  1266. let freq = parseFloat(freqEl1.value); if (isNaN(freq) || freq <= 0) freq = 1;
  1267. let speedEl = document.getElementById('calcSpeedCustom'); let speedCustom = speedEl ? parseFloat(speedEl.value) || 343 : 343;
  1268. safeSetText('calcResAir', `${(speedCustom / freq).toFixed(2)}`);
  1269. let T = (1 / freq).toFixed(5); safeSetText('calcResPeriod', T);
  1270. let omega = (2 * Math.PI * freq).toFixed(1); safeSetText('calcResOmega', `${i18n[currentLang].omegaLabel} ~${omega} ${i18n[currentLang].radS}`);
  1271. const sel = document.getElementById('mediumSelect'); let descLabel = document.getElementById('lblCalcAirDesc');
  1272. if (descLabel) {
  1273. if (sel && sel.value !== 'custom' && sel.options[sel.selectedIndex]) {
  1274. let text = sel.options[sel.selectedIndex].innerText; descLabel.innerText = currentLang === 'ru' ? `Среда: ${text}` : `Medium: ${text}`;
  1275. } else {
  1276. descLabel.innerText = currentLang === 'ru' ? `Скорость ${speedCustom} м/с:` : `Speed ${speedCustom} m/s:`;
  1277. }
  1278. }
  1279. }
  1280. 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'); }
  1281. 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); }
  1282. 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(); }
  1283. function updateWaveFromSlider(type) {
  1284. 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); } }
  1285. 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); } }
  1286. 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); } }
  1287. updateTelemetryUI(); updateWaveAudio();
  1288. }
  1289. function updateWaveFromInput(type) {
  1290. 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; } } }
  1291. 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; } } }
  1292. 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; } } }
  1293. playAudioCue('calcKey'); updateTelemetryUI(); updateWaveAudio();
  1294. }
  1295. 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'); }
  1296. function togglePause() { isPaused = !isPaused; updateDynamicButtons(); playAudioCue('click'); }
  1297. function setPreset(type) {
  1298. 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');
  1299. if (type === 'sine') { waves[0] = { f: 440, a: 80, p: 0, type: 'sine' }; waves[1] = { f: 880, a: 0, p: 0, type: 'sine' }; waves[2] = { f: 1320, a: 0, p: 0, type: 'sine' }; isClipped = false; }
  1300. else if (type === 'square') { waves[0] = { f: 400, a: 100, p: 0, type: 'square' }; waves[1] = { f: 1200, a: 33, p: 0, type: 'square' }; waves[2] = { f: 2000, a: 20, p: 0, type: 'square' }; }
  1301. else if (type === 'triangle') { waves[0] = { f: 400, a: 100, p: 0, type: 'triangle' }; waves[1] = { f: 1200, a: 11, p: 180, type: 'triangle' }; waves[2] = { f: 2000, a: 4, p: 0, type: 'triangle' }; }
  1302. else if (type === 'sawtooth') { waves[0] = { f: 400, a: 100, p: 0, type: 'sawtooth' }; waves[1] = { f: 800, a: 50, p: 180, type: 'sawtooth' }; waves[2] = { f: 1200, a: 33, p: 0, type: 'sawtooth' }; }
  1303. updateLabels(); updateDynamicButtons(); updateWaveAudio(); playAudioCue('mainPulse');
  1304. }
  1305. document.querySelectorAll('.presets-row button').forEach(btn => {
  1306. btn.addEventListener('click', () => { let id = btn.id.replace('pr_', ''); waves[currentWave].type = id; });
  1307. });
  1308. function toggleClip() { isClipped = !isClipped; updateLabels(); updateDynamicButtons(); updateWaveAudio(); playAudioCue('mainPulse'); }
  1309. function resetAll() {
  1310. waves = [{ f: 440, a: 80, p: 0, type: 'sine' }, { f: 880, a: 0, p: 0, type: 'sine' }, { f: 1320, a: 0, p: 0, type: 'sine' }]; isClipped = false; isLissajous = false; clearBackground();
  1311. let lisBtn = document.getElementById('btnLissajous'); if(lisBtn) { lisBtn.classList.remove('active'); }
  1312. safeSetText('lblLisMode', ""); animSpeedMultiplier = 1.0;
  1313. let elSpeed = document.getElementById('speed'); if(elSpeed) elSpeed.value = 1.0; updateSpeedValue(1.0);
  1314. setPreset('sine'); if (isPaused) togglePause(); resetCanvasSizes(); switchTab(0); updateWaveAudio(); updateDynamicButtons(); playAudioCue('error');
  1315. }
  1316. function updateWaveAudio() {
  1317. if (!waveCtx || oscillators.length === 0 || !masterWaveShaper) return;
  1318. const now = waveCtx.currentTime;
  1319. if (isClipped) { masterWaveShaper.curve = makeDistortionCurve(400); }
  1320. else { masterWaveShaper.curve = null; }
  1321. for (let i = 0; i < 3; i++) {
  1322. oscillators[i].osc.frequency.setTargetAtTime(waves[i].f, now, 0.03);
  1323. oscillators[i].osc.type = waves[i].type || 'sine';
  1324. let targetVol = 0; if (isAudioOn && waves[i].a > 0) { targetVol = (waves[i].a / 100) * 0.12; }
  1325. oscillators[i].gain.gain.setTargetAtTime(targetVol, now, 0.05);
  1326. }
  1327. }
  1328. function toggleAudio() { try { if (waveCtx && waveCtx.state === 'suspended') waveCtx.resume(); } catch(e){} isAudioOn = !isAudioOn; updateDynamicButtons(); updateWaveAudio(); playAudioCue('action'); }
  1329. function updateColorsCache() { cachedBaseColor = getComputedStyle(document.documentElement).getPropertyValue('--base-wave').trim() || '#2196F3'; }
  1330. function changeTheme(t) { document.documentElement.setAttribute('data-theme', t); buildSelectOptions(); updateColorsCache(); playAudioCue('action'); }
  1331. function changeFilter(f) { let bl = document.getElementById('bg-layer'); if(bl) bl.style.filter = f; buildSelectOptions(); playAudioCue('click'); }
  1332. 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'); }
  1333. 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]); } }
  1334. function downloadGraph(id) {
  1335. const c = document.getElementById(id); if(!c) return;
  1336. let btn = document.querySelector('.btn-camera-snap');
  1337. if (btn) {
  1338. let flash = document.createElement('div');
  1339. flash.className = 'camera-flash-overlay';
  1340. flash.style.animation = 'flashAnim 0.4s ease-out';
  1341. c.parentElement.appendChild(flash);
  1342. setTimeout(() => flash.remove(), 400);
  1343. }
  1344. const l = document.createElement('a'); l.download = 'HarmoniX_Graph.png';
  1345. const tc = document.createElement('canvas'); tc.width = c.width; tc.height = c.height;
  1346. const tCtx = tc.getContext('2d'); tCtx.fillStyle = '#ffffff'; tCtx.fillRect(0,0,tc.width,tc.height);
  1347. tCtx.drawImage(c,0,0); l.href = tc.toDataURL(); l.click();
  1348. }
  1349. function getWaveValue(type, phase) {
  1350. switch (type) {
  1351. case 'square': return Math.sign(Math.sin(phase));
  1352. case 'sawtooth': return 2 * (phase / (2 * Math.PI) - Math.floor(phase / (2 * Math.PI) + 0.5));
  1353. case 'triangle': return 1 - 4 * Math.abs(Math.round(phase / (2 * Math.PI)) - phase / (2 * Math.PI));
  1354. case 'sine':
  1355. default: return Math.sin(phase);
  1356. }
  1357. }
  1358. let lastTelemUpdate = 0;
  1359. function draw() {
  1360. if (!isPaused) time += (BASE_ANIM_SPEED * animSpeedMultiplier);
  1361. let c1 = document.getElementById('canvas-single'); let c2 = document.getElementById('canvas-sum'); let c3 = document.getElementById('canvas-spectrum');
  1362. let ctx1 = c1 ? c1.getContext('2d') : null; if(ctx1) ctx1.clearRect(0, 0, c1.width, c1.height);
  1363. let ctx2 = c2 ? c2.getContext('2d') : null; if(ctx2) ctx2.clearRect(0, 0, c2.width, c2.height);
  1364. let ctx3 = c3 ? c3.getContext('2d') : null; if(ctx3) ctx3.clearRect(0, 0, c3.width, c3.height);
  1365. let baseColor = cachedBaseColor; let sumColor = "#E91E63"; let gridColor = "rgba(128,128,128,0.22)";
  1366. const wParams = waves.map(w => ({ vf: (w.f / 200) || 1, pr: w.p * (Math.PI / 180), amp: (w.a / 100), type: w.type || 'sine' }));
  1367. const c1mid = c1 ? c1.height / 2 : 0; const c1amp = c1 ? c1.height / 2.5 : 0;
  1368. const c2mid = c2 ? c2.height / 2 : 0; const c2amp = c2 ? c2.height / 2.5 : 0;
  1369. if (isGridOn) {
  1370. if(ctx1) { ctx1.beginPath(); ctx1.strokeStyle = gridColor; ctx1.moveTo(0, c1mid); ctx1.lineTo(c1.width, c1mid); ctx1.stroke(); }
  1371. if(ctx2) { ctx2.beginPath(); ctx2.strokeStyle = gridColor; ctx2.moveTo(0, c2mid); ctx2.lineTo(c2.width, c2mid); ctx2.stroke(); }
  1372. if(ctx3) { ctx3.beginPath(); ctx3.strokeStyle = gridColor; ctx3.moveTo(0, c3.height - 18); ctx3.lineTo(c3.width, c3.height - 18); ctx3.stroke(); }
  1373. }
  1374. if (isGlowOn) {
  1375. if(ctx1) { ctx1.shadowBlur = 10; ctx1.shadowColor = baseColor; }
  1376. if(ctx2) { ctx2.shadowBlur = 10; ctx2.shadowColor = sumColor; }
  1377. if(ctx3) { ctx3.shadowBlur = 10; ctx3.shadowColor = "#4CAF50"; }
  1378. } else {
  1379. if(ctx1) ctx1.shadowBlur = 0; if(ctx2) ctx2.shadowBlur = 0; if(ctx3) ctx3.shadowBlur = 0;
  1380. }
  1381. let singleInstantY = 0;
  1382. if(ctx1 && c1.width > 0) {
  1383. ctx1.beginPath(); ctx1.strokeStyle = baseColor; ctx1.lineWidth = 2.5;
  1384. if (isLissajous) {
  1385. 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);
  1386. 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); }
  1387. } else {
  1388. for (let x = 0; x < c1.width; x += 2) {
  1389. let phaseCalc = x * 0.02 * wParams[currentWave].vf - time + wParams[currentWave].pr;
  1390. let y = getWaveValue(wParams[currentWave].type, phaseCalc) * wParams[currentWave].amp * c1amp;
  1391. if (x === 0) { ctx1.moveTo(x, c1mid - y); singleInstantY = y / c1amp; } else ctx1.lineTo(x, c1mid - y);
  1392. }
  1393. }
  1394. ctx1.stroke();
  1395. }
  1396. let sumInstantY = 0;
  1397. if(ctx2 && c2.width > 0) {
  1398. let clipLimit = c2.height / 4; ctx2.beginPath();
  1399. ctx2.strokeStyle = isMaskingTZI ? '#00E676' : sumColor; ctx2.lineWidth = isMaskingTZI ? 1.5 : 2.5;
  1400. for (let x = 0; x < c2.width; x += 2) {
  1401. let sumY = 0;
  1402. for (let i = 0; i < 3; i++) {
  1403. let phaseCalc = x * 0.02 * wParams[i].vf - time + wParams[i].pr;
  1404. sumY += getWaveValue(wParams[i].type, phaseCalc) * wParams[i].amp * c2amp;
  1405. }
  1406. if (isClipped) { if (sumY > clipLimit) sumY = clipLimit; if (sumY < -clipLimit) sumY = -clipLimit; }
  1407. if (isMaskingTZI) { sumY += (Math.random() * 40 - 20); }
  1408. if (x === 0) { ctx2.moveTo(x, c2mid - sumY/1.5); sumInstantY = sumY / c2amp; } else ctx2.lineTo(x, c2mid - sumY/1.5);
  1409. }
  1410. ctx2.stroke();
  1411. }
  1412. let nowTime = performance.now();
  1413. if (showLabels && (nowTime - lastTelemUpdate > 90)) {
  1414. lastTelemUpdate = nowTime;
  1415. let tY = document.getElementById('telY'); if(tY) tY.innerText = isLissajous ? "2D" : (singleInstantY >= 0 ? "+" : "") + singleInstantY.toFixed(2);
  1416. let tSY = document.getElementById('telSumY'); if(tSY) tSY.innerText = (sumInstantY >= 0 ? "+" : "") + sumInstantY.toFixed(2);
  1417. let activePeaks = waves.filter(item => item.a > 0).length; if (isClipped && waves[0].a > 0) activePeaks += 4;
  1418. let tSP = document.getElementById('telSpecPeaks'); if(tSP) tSP.innerText = activePeaks;
  1419. let thdEl = document.getElementById('telemTHD');
  1420. let limitEl = document.getElementById('telemLimit');
  1421. if (isClipped) {
  1422. if(thdEl) { thdEl.style.display = 'inline-flex'; document.getElementById('telTHDVal').innerText = "≈ 43%"; }
  1423. if(limitEl) { limitEl.style.display = 'inline-flex'; document.getElementById('telLimitVal').innerText = "± 25%"; }
  1424. } else {
  1425. if(thdEl) thdEl.style.display = 'none';
  1426. if(limitEl) limitEl.style.display = 'none';
  1427. }
  1428. }
  1429. if(ctx3 && c3.width > 0) {
  1430. ctx3.shadowBlur = 0;
  1431. 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); }
  1432. ctx3.fillStyle = "#4CAF50";
  1433. if (isGlowOn) { ctx3.shadowBlur = 10; ctx3.shadowColor = "#4CAF50"; }
  1434. 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); } }
  1435. if (isClipped) {
  1436. ctx3.fillStyle = "rgba(255, 87, 34, 0.9)";
  1437. if (isGlowOn) { ctx3.shadowBlur = 10; ctx3.shadowColor = "rgba(255, 87, 34, 1)"; }
  1438. for (let w = 0; w < 3; w++) {
  1439. if (wParams[w].amp > 0) {
  1440. for (let i = 3; i <= 15; i += 2) {
  1441. let fakeF = waves[w].f * i;
  1442. if (fakeF > 20000) break;
  1443. let xPos = (fakeF / 20000) * c3.width;
  1444. let h = (waves[w].a / (i*1.5) / 100) * (c3.height - 32);
  1445. ctx3.fillRect(Math.max(xPos, 5), c3.height - 18 - h, 6, h);
  1446. }
  1447. }
  1448. }
  1449. }
  1450. if (isMaskingTZI) {
  1451. ctx3.fillStyle = "rgba(0, 230, 118, 0.4)";
  1452. 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); }
  1453. }
  1454. ctx3.shadowBlur = 0;
  1455. }
  1456. animationFrameId = requestAnimationFrame(draw);
  1457. }
  1458. function generateDynamicQuestion(index) {
  1459. const freqs = [10, 20, 50, 100, 200, 440, 500, 1000]; const f = freqs[index % freqs.length]; const v = [340, 1480, 5900][index % 3]; const isRu = currentLang === 'ru';
  1460. const templates = [
  1461. () => { 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' } },
  1462. () => { 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' } },
  1463. () => { 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' } },
  1464. () => { 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' } },
  1465. () => { 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' } },
  1466. () => { 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)' } },
  1467. () => { 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' } },
  1468. () => { 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)' } }
  1469. ];
  1470. return templates[index % templates.length]();
  1471. }
  1472. function shuffle(a) { return a.sort(() => Math.random() - 0.5); }
  1473. let quizAnswers = {}; let quizQuestionsList = []; let currentQuestionIdx = 0;
  1474. let quizTimerInterval = null; let quizTimeLeft = 0; let quizTotalTime = 0;
  1475. function startQuizTimer(seconds) {
  1476. clearInterval(quizTimerInterval);
  1477. const bar = document.getElementById('quizTimerBar'); const val = document.getElementById('quizTimerVal'); const prog = document.getElementById('quizTimerProgress');
  1478. if(!bar || !val) return;
  1479. val.className = 'timer-display';
  1480. 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)'; }
  1481. if (seconds <= 0) { bar.style.display = 'none'; return; }
  1482. bar.style.display = 'flex'; quizTimeLeft = seconds; quizTotalTime = seconds;
  1483. function updateTimerDisplay() {
  1484. let m = Math.floor(quizTimeLeft / 60); let s = quizTimeLeft % 60;
  1485. val.innerText = `${m < 10 ? '0' : ''}${m}:${s < 10 ? '0' : ''}${s}`;
  1486. if(prog) {
  1487. let pct = (quizTimeLeft / quizTotalTime) * 100; prog.style.width = `${pct}%`;
  1488. 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)'; }
  1489. 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)'; }
  1490. else { prog.style.background = 'linear-gradient(90deg, #FF5722 0%, #F44336 100%)'; prog.style.boxShadow = '0 0 12px rgba(244, 67, 54, 0.8)'; }
  1491. }
  1492. if (quizTimeLeft <= 10) { val.className = 'timer-display timer-red-blink'; playAudioCue('tick'); }
  1493. else if (quizTimeLeft <= 30) { val.className = 'timer-display timer-orange'; }
  1494. else if (quizTimeLeft === 60) { playAudioCue('warn'); val.className = 'timer-display timer-orange'; }
  1495. else { val.className = 'timer-display'; }
  1496. }
  1497. updateTimerDisplay();
  1498. quizTimerInterval = setInterval(() => {
  1499. quizTimeLeft--;
  1500. if (quizTimeLeft <= 0) { clearInterval(quizTimerInterval); checkTest(); alert(i18n[currentLang].testTimeUp); }
  1501. else { updateTimerDisplay(); }
  1502. }, 1000);
  1503. }
  1504. function openTestMenu(){
  1505. clearInterval(quizTimerInterval); let ov = document.getElementById('test-overlay'); if(!ov) return;
  1506. ov.style.display = 'flex'; setTimeout(()=> ov.classList.add('show'), 10);
  1507. const startMenu = document.getElementById('test-start-menu'); if (startMenu) startMenu.style.display = 'block';
  1508. const modesBox = document.getElementById('testModesContainer'); if (modesBox) modesBox.style.display = 'grid';
  1509. document.getElementById('quiz-process').style.display = 'none';
  1510. document.getElementById('test-result-screen').style.display = 'none';
  1511. document.getElementById('quiz-card-wrapper').style.display = 'block';
  1512. document.getElementById('quiz-nav-controls').style.display = 'flex';
  1513. let mc = document.querySelector('#test-overlay .modal-content'); if(mc) mc.classList.add('pop-animation');
  1514. playAudioCue('menuClick');
  1515. }
  1516. function startQuiz(n){
  1517. document.getElementById('test-start-menu').style.display = 'none';
  1518. document.getElementById('quiz-process').style.display = 'block';
  1519. document.getElementById('test-result-screen').style.display = 'none';
  1520. document.getElementById('quiz-card-wrapper').style.display = 'block';
  1521. document.getElementById('quiz-nav-controls').style.display = 'flex';
  1522. quizAnswers = {}; quizQuestionsList = []; currentQuestionIdx = 0;
  1523. for (let i = 0; i < n; i++) {
  1524. let randomIdx = Math.floor(Math.random() * 1000);
  1525. const item = generateDynamicQuestion(randomIdx);
  1526. item.opts = shuffle([...item.opts]);
  1527. quizQuestionsList.push(item);
  1528. }
  1529. renderCurrentQuestion();
  1530. if (n === 5) startQuizTimer(0); else if (n === 15) startQuizTimer(300); else if (n === 30) startQuizTimer(600); else if (n === 50) startQuizTimer(900);
  1531. playAudioCue('glassBell');
  1532. }
  1533. function renderCurrentQuestion() {
  1534. const container = document.getElementById('quiz-container'); if(!container) return; container.innerHTML = '';
  1535. const item = quizQuestionsList[currentQuestionIdx]; const total = quizQuestionsList.length;
  1536. let card = document.createElement('div'); card.className = 'question glass-element pop-animation';
  1537. let meta = document.createElement('div'); meta.className = 'question-meta';
  1538. meta.innerHTML = `<span>${i18n[currentLang].qWord} ${currentQuestionIdx + 1} ${i18n[currentLang].qOf} ${total}</span>`;
  1539. card.appendChild(meta);
  1540. let qText = document.createElement('p'); qText.innerText = item.q; card.appendChild(qText);
  1541. item.opts.forEach((opt, optIdx) => {
  1542. let lbl = document.createElement('label'); lbl.className = 'quiz-option glass-element smooth-transition';
  1543. if (quizAnswers[currentQuestionIdx] !== undefined && quizAnswers[currentQuestionIdx] === optIdx) { lbl.classList.add('selected'); }
  1544. lbl.onclick = function() {
  1545. quizAnswers[currentQuestionIdx] = optIdx;
  1546. card.querySelectorAll('.quiz-option').forEach(el => el.classList.remove('selected'));
  1547. lbl.classList.add('selected'); playAudioCue('calcKey');
  1548. };
  1549. lbl.innerHTML = `<input type="radio" name="q_opt" ${quizAnswers[currentQuestionIdx] === optIdx ? 'checked' : ''}><div class="custom-radio"></div><span class="option-text">${opt.t}</span>`;
  1550. card.appendChild(lbl);
  1551. });
  1552. container.appendChild(card); updateQuizStepIndicator();
  1553. }
  1554. function updateQuizStepIndicator() {
  1555. const total = quizQuestionsList.length;
  1556. const ind = document.getElementById('quiz-step-indicator'); if(ind) ind.innerText = `${currentQuestionIdx + 1} / ${total}`;
  1557. const btnPrev = document.getElementById('btnPrevQ'); if(btnPrev) btnPrev.disabled = (currentQuestionIdx === 0);
  1558. const btnNext = document.getElementById('btnNextQ');
  1559. if(btnNext) {
  1560. if (currentQuestionIdx === total - 1) {
  1561. btnNext.innerHTML = `<span>${i18n[currentLang].btnCheck}</span> ✅`;
  1562. btnNext.onclick = function() { checkTest(); playAudioCue('glassBell'); };
  1563. } else {
  1564. btnNext.innerHTML = `<span>${i18n[currentLang].btnNext}</span> ▶`;
  1565. btnNext.onclick = function() { nextQuestion(); playAudioCue('mainPulse'); };
  1566. }
  1567. }
  1568. }
  1569. function nextQuestion() { if (currentQuestionIdx < quizQuestionsList.length - 1) { currentQuestionIdx++; renderCurrentQuestion(); } }
  1570. function prevQuestion() { if (currentQuestionIdx > 0) { currentQuestionIdx--; renderCurrentQuestion(); } }
  1571. function closeTest(){
  1572. clearInterval(quizTimerInterval); let ov = document.getElementById('test-overlay'); if(!ov) return;
  1573. ov.classList.remove('show');
  1574. setTimeout(()=> { ov.style.display = 'none'; let mc = document.querySelector('#test-overlay .modal-content'); if(mc) mc.classList.remove('pop-animation'); }, 300);
  1575. playAudioCue('click');
  1576. }
  1577. function animateScoreCounter(targetPercent, onFinish) {
  1578. const text = document.getElementById('scoreCircleText');
  1579. const circle = document.getElementById('scoreProgress');
  1580. let color = '#f44336';
  1581. if (targetPercent >= 90) color = '#00E676';
  1582. else if (targetPercent >= 60) color = '#FF9800';
  1583. circle.style.stroke = color;
  1584. text.style.color = color;
  1585. text.style.textShadow = `0 0 15px ${color}`;
  1586. circle.parentElement.style.filter = `drop-shadow(0 0 12px ${color})`;
  1587. const circumference = 2 * Math.PI * 54;
  1588. circle.style.strokeDasharray = `${circumference} ${circumference}`;
  1589. let current = 0; text.innerText = "0%";
  1590. circle.style.strokeDashoffset = circumference;
  1591. setTimeout(() => { circle.style.strokeDashoffset = circumference - (targetPercent / 100) * circumference; }, 50);
  1592. const stepTime = Math.max(15, Math.floor(1200 / (targetPercent || 1)));
  1593. const timer = setInterval(() => {
  1594. if (current < targetPercent) {
  1595. current++; text.innerText = `${current}%`;
  1596. if (current % 4 === 0) playAudioCue('calcKey');
  1597. } else {
  1598. clearInterval(timer); text.innerText = `${targetPercent}%`;
  1599. if (onFinish) onFinish();
  1600. }
  1601. }, stepTime);
  1602. }
  1603. function checkTest(){
  1604. clearInterval(quizTimerInterval);
  1605. const total = quizQuestionsList.length; let score = 0;
  1606. 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;'>`;
  1607. quizQuestionsList.forEach((item, idx) => {
  1608. const chosenOptIdx = quizAnswers[idx]; const isAnswered = (chosenOptIdx !== undefined); const isCorrect = isAnswered && item.opts[chosenOptIdx].c;
  1609. if (isCorrect) { score++; } else {
  1610. const correctOpt = item.opts.find(o => o.c);
  1611. const skippedTag = !isAnswered ? ` <span style="color:#f44336; font-weight:bold;">[${i18n[currentLang].testSkipped}]</span>` : '';
  1612. 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>`;
  1613. }
  1614. });
  1615. explanationHTML += `</ul>`;
  1616. document.getElementById('quiz-card-wrapper').style.display = 'none';
  1617. document.getElementById('quiz-nav-controls').style.display = 'none';
  1618. document.getElementById('quizTimerBar').style.display = 'none';
  1619. const resScreen = document.getElementById('test-result-screen');
  1620. resScreen.style.display = 'block'; resScreen.classList.add('pop-animation');
  1621. let pct = Math.round((score / total) * 100);
  1622. const resText = document.getElementById('test-result-text');
  1623. resText.innerHTML = `${i18n[currentLang].testResTitle} ${score} ${i18n[currentLang].testResOf} ${total}<br>`;
  1624. animateScoreCounter(pct, () => {
  1625. if (pct >= 90) { resText.innerHTML += `<span style="color:#00E676;">${i18n[currentLang].testRes90}</span>`; playAudioCue('glassBell'); }
  1626. else if (pct >= 60) { resText.innerHTML += `<span style="color:#FF9800;">${i18n[currentLang].testRes60}</span>`; playAudioCue('glassBell'); }
  1627. else { resText.innerHTML += `<span style="color:#f44336;">${i18n[currentLang].testResLow}</span>`; playAudioCue('warn'); }
  1628. });
  1629. document.getElementById('quiz-explanation').innerHTML = explanationHTML;
  1630. }
  1631. </script>
  1632. </body>
  1633. </html>