index.html 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. <!DOCTYPE html>
  2. <html lang="ru">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>HarmoniX | Волновой Спектр</title>
  7. <style>
  8. :root {
  9. --bg-color: #fdfaf6;
  10. --panel-bg: rgba(255, 255, 255, 0.75);
  11. --text-color: #333;
  12. --border-color: rgba(204, 204, 204, 0.5);
  13. --accent: #4CAF50;
  14. --base-wave: #2196F3;
  15. --btn-glow: rgba(76, 175, 80, 0.6);
  16. --canvas-bg: rgba(250, 250, 250, 0.85);
  17. }
  18. [data-theme="dark"] {
  19. --bg-color: #121212; --panel-bg: rgba(30, 30, 30, 0.75);
  20. --text-color: #eee; --border-color: rgba(68, 68, 68, 0.5); --accent: #bb86fc;
  21. --base-wave: #03dac6; --btn-glow: rgba(187, 134, 252, 0.6); --canvas-bg: rgba(44, 44, 44, 0.85);
  22. }
  23. [data-theme="neon"] {
  24. --bg-color: #000016; --panel-bg: rgba(10, 10, 30, 0.75);
  25. --text-color: #0ff; --border-color: rgba(255, 0, 255, 0.5); --accent: #f0f;
  26. --base-wave: #0ff; --btn-glow: rgba(255, 0, 255, 0.8); --canvas-bg: rgba(0, 0, 0, 0.85);
  27. }
  28. body, html { margin: 0; padding: 0; width: 100%; height: 100%; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; overflow-x: hidden; color: var(--text-color); }
  29. #bg-layer {
  30. position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  31. background-color: var(--bg-color);
  32. background-size: cover; background-position: center;
  33. z-index: -2; transition: filter 0.3s;
  34. }
  35. /* --- Анимированный градиент для логотипа --- */
  36. .gradient-text {
  37. background: linear-gradient(45deg, #ff00cc, #3333ff, #00ffcc, #ff00cc);
  38. background-size: 300% 300%;
  39. -webkit-background-clip: text;
  40. -webkit-text-fill-color: transparent;
  41. animation: gradientFlow 5s ease infinite;
  42. font-weight: bold;
  43. }
  44. @keyframes gradientFlow {
  45. 0% { background-position: 0% 50%; }
  46. 50% { background-position: 100% 50%; }
  47. 100% { background-position: 0% 50%; }
  48. }
  49. #welcome-screen {
  50. position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  51. display: flex; flex-direction: column; justify-content: center; align-items: center;
  52. background: var(--bg-color); z-index: 1000; transition: opacity 0.5s;
  53. }
  54. .logo { font-size: 80px; margin-bottom: 20px; text-shadow: 0 0 30px rgba(0, 212, 255, 0.5); }
  55. .app-title { font-size: 56px; margin: 0; }
  56. .app-subtitle { font-size: 20px; color: var(--text-color); margin-bottom: 40px; opacity: 0.8; }
  57. .btn-enter {
  58. padding: 15px 40px; font-size: 22px; font-weight: bold; color: #fff;
  59. background: linear-gradient(45deg, #ff00cc, #3333ff); border: none; border-radius: 30px; cursor: pointer;
  60. box-shadow: 0 0 20px rgba(51, 51, 255, 0.6); transition: transform 0.2s;
  61. }
  62. .btn-enter:hover { transform: scale(1.05); }
  63. #main-app { display: none; padding: 20px; box-sizing: border-box; height: 100%; }
  64. .top-bar {
  65. display: flex; justify-content: space-between; align-items: center;
  66. background: var(--panel-bg); padding: 10px 20px; border-radius: 8px;
  67. border: 1px solid var(--border-color); margin-bottom: 15px;
  68. box-shadow: 0 4px 10px rgba(0,0,0,0.1); backdrop-filter: blur(10px);
  69. }
  70. .nav-logo { font-size: 28px; cursor: pointer; display: flex; align-items: center; gap: 10px;}
  71. .settings-group { display: flex; gap: 15px; align-items: center; flex-wrap: wrap; }
  72. .settings-group select, .settings-group input[type="file"] {
  73. background: transparent; color: var(--text-color); border: 1px solid var(--border-color); padding: 5px; border-radius: 4px;
  74. }
  75. .grid-container {
  76. display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: auto auto;
  77. gap: 15px; min-height: calc(100vh - 120px);
  78. }
  79. .panel {
  80. background: var(--panel-bg); border: 1px solid var(--border-color);
  81. padding: 15px; border-radius: 8px; display: flex; flex-direction: column;
  82. backdrop-filter: blur(10px);
  83. }
  84. .panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
  85. /* --- Контейнер для ручного изменения размера графиков --- */
  86. .canvas-wrapper {
  87. resize: both; /* Разрешаем менять размер вручную */
  88. overflow: hidden;
  89. width: 100%;
  90. height: 200px;
  91. min-height: 150px;
  92. min-width: 250px;
  93. background: var(--canvas-bg);
  94. border: 1px solid var(--border-color);
  95. border-radius: 4px;
  96. position: relative;
  97. }
  98. canvas {
  99. display: block; width: 100%; height: 100%;
  100. }
  101. .tabs { display: flex; gap: 5px; margin: 10px 0; }
  102. .tab { padding: 8px 15px; cursor: pointer; background: transparent; border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-color); transition: 0.2s; }
  103. .tab.active { background: var(--accent); color: #fff; font-weight: bold; border-color: var(--accent); }
  104. .control-row { margin-bottom: 15px; display: flex; align-items: center; }
  105. label { display: inline-block; width: 140px; font-weight: bold; }
  106. input[type=range] { flex-grow: 1; margin-right: 15px; cursor: pointer; }
  107. .btn { padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; color: white; margin-right: 5px; transition: 0.2s; }
  108. .btn:hover { filter: brightness(1.1); transform: translateY(-1px); }
  109. .btn-audio { background: #2196F3; } .btn-dist { background: #9C27B0; } .btn-reset { background: #607D8B; } .btn-icon { background: transparent; border: 1px solid var(--border-color); color: var(--text-color); }
  110. .btn-audio.active { background: #f44336; box-shadow: 0 0 15px rgba(244, 67, 54, 0.6); }
  111. .btn-dist.active { background: #d32f2f; box-shadow: 0 0 15px rgba(211, 47, 47, 0.6); }
  112. </style>
  113. </head>
  114. <body>
  115. <div id="bg-layer"></div>
  116. <div id="welcome-screen">
  117. <div class="logo">🌊</div>
  118. <h1 class="app-title gradient-text">HarmoniX</h1>
  119. <h2 class="app-subtitle">Волновой Спектр / Механические волны и Фурье</h2>
  120. <button class="btn-enter" onclick="startApp()">Войти в приложение</button>
  121. </div>
  122. <div id="main-app">
  123. <nav class="top-bar">
  124. <div class="nav-logo gradient-text" onclick="location.reload()"><span>🌊</span> HarmoniX</div>
  125. <div class="settings-group">
  126. <label>🎨 Тема:</label>
  127. <select id="themeSelect" onchange="changeTheme(this.value)">
  128. <option value="default">Светлая</option>
  129. <option value="dark">Темная</option>
  130. <option value="neon">Неон</option>
  131. </select>
  132. <label>🖼️ Фон:</label>
  133. <input type="file" id="bgUpload" accept="image/*" onchange="uploadBackground(event)">
  134. <select id="filterSelect" onchange="changeFilter(this.value)">
  135. <option value="none">Без фильтра</option>
  136. <option value="blur(8px)">Размытие</option>
  137. <option value="grayscale(100%)">Ч/Б</option>
  138. <option value="sepia(80%)">Сепия</option>
  139. <option value="hue-rotate(90deg)">Инверсия цвета</option>
  140. </select>
  141. <button class="btn btn-icon" onclick="toggleFullscreen()">🔲 На весь экран</button>
  142. </div>
  143. </nav>
  144. <div class="grid-container">
  145. <div class="panel">
  146. <div class="panel-header"><h3 style="margin:0;">1. Базовое колебание</h3><span style="font-size:10px; opacity:0.5;">Потяните за правый нижний угол графика ↘</span></div>
  147. <div class="canvas-wrapper">
  148. <canvas id="canvas-single"></canvas>
  149. </div>
  150. </div>
  151. <div class="panel">
  152. <div class="panel-header">
  153. <h3 style="margin:0;">3. Теорема Фурье</h3>
  154. <button class="btn btn-icon" style="padding: 4px 8px; font-size:12px;" onclick="downloadGraph('canvas-sum')">💾 Скачать график</button>
  155. </div>
  156. <div class="canvas-wrapper">
  157. <canvas id="canvas-sum"></canvas>
  158. </div>
  159. </div>
  160. <div class="panel">
  161. <h3 style="margin:0">2. Управление</h3>
  162. <div class="tabs">
  163. <div class="tab active" onclick="switchTab(0)">V1 (Волна 1)</div>
  164. <div class="tab" onclick="switchTab(1)">V2 (Волна 2)</div>
  165. <div class="tab" onclick="switchTab(2)">V3 (Волна 3)</div>
  166. </div>
  167. <div class="control-row">
  168. <label>Шаг частоты:</label>
  169. <input type="radio" name="step" value="10" onchange="updateStep(10); playUISound('click')"> 10 Гц
  170. <input type="radio" name="step" value="100" onchange="updateStep(100); playUISound('click')" checked> 100 Гц
  171. </div>
  172. <div class="control-row">
  173. <label>Частота V:</label>
  174. <input type="range" id="freq" min="0" max="20000" step="100" value="440" oninput="updateWave(); playSliderSound(this.value, 20000)">
  175. <div style="width: 60px; text-align:right"><span id="valV">440</span> Гц</div>
  176. </div>
  177. <div class="control-row">
  178. <label>Амплитуда A:</label>
  179. <input type="range" id="amp" min="0" max="100" value="80" oninput="updateWave(); playSliderSound(this.value, 100, 0, 'low')">
  180. <div style="width: 60px; text-align:right"><span id="valA">80</span>%</div>
  181. </div>
  182. <div>
  183. <button class="btn btn-audio" onclick="toggleAudio(); playUISound('action')" id="btnAudio">🔊 Динамик</button>
  184. <button class="btn btn-dist" onclick="toggleClip(); playUISound('action')" id="btnClip">⚡ Клиппинг (Искажение)</button>
  185. <button class="btn btn-reset" onclick="resetAll(); playUISound('error')">🔄 Сброс</button>
  186. </div>
  187. </div>
  188. <div class="panel">
  189. <h3 style="margin:0; margin-bottom: 10px;">4. Спектр</h3>
  190. <div class="canvas-wrapper">
  191. <canvas id="canvas-spectrum"></canvas>
  192. </div>
  193. </div>
  194. </div>
  195. </div>
  196. <script>
  197. let waves = [{ f: 440, a: 80 }, { f: 880, a: 0 }, { f: 1320, a: 0 }];
  198. let currentWave = 0, freqStep = 100, time = 0;
  199. let isClipped = false, isAudioOn = false;
  200. let uiAudioCtx = null, audioCtx = null, oscillators = [];
  201. let animationFrameId = null;
  202. // --- Умное изменение размеров Canvas (ResizeObserver) ---
  203. const resizeObserver = new ResizeObserver(entries => {
  204. for (let entry of entries) {
  205. const wrapper = entry.target;
  206. const canvas = wrapper.querySelector('canvas');
  207. if (canvas) {
  208. canvas.width = wrapper.clientWidth;
  209. canvas.height = wrapper.clientHeight;
  210. }
  211. }
  212. });
  213. function startApp() {
  214. document.getElementById('welcome-screen').style.opacity = '0';
  215. setTimeout(() => {
  216. document.getElementById('welcome-screen').style.display = 'none';
  217. document.getElementById('main-app').style.display = 'block';
  218. // Подключаем наблюдение за размерами всех оберток графиков
  219. document.querySelectorAll('.canvas-wrapper').forEach(wrapper => {
  220. resizeObserver.observe(wrapper);
  221. });
  222. uiAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
  223. playUISound('action');
  224. if (!animationFrameId) draw();
  225. }, 500);
  226. }
  227. function uploadBackground(event) {
  228. const file = event.target.files[0];
  229. if (file) {
  230. const reader = new FileReader();
  231. reader.onload = function(e) {
  232. document.getElementById('bg-layer').style.backgroundImage = `url('${e.target.result}')`;
  233. playUISound('action');
  234. };
  235. reader.readAsDataURL(file);
  236. }
  237. event.target.value = '';
  238. }
  239. function playUISound(type) {
  240. if (!uiAudioCtx) return;
  241. const osc = uiAudioCtx.createOscillator(), gain = uiAudioCtx.createGain();
  242. osc.connect(gain); gain.connect(uiAudioCtx.destination);
  243. let now = uiAudioCtx.currentTime;
  244. if (type === 'click' || type === 'hover') {
  245. osc.type = 'sine'; osc.frequency.setValueAtTime(800, now);
  246. gain.gain.setValueAtTime(0.05, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
  247. osc.start(now); osc.stop(now + 0.1);
  248. } else if (type === 'action') {
  249. osc.type = 'triangle'; osc.frequency.setValueAtTime(400, now); osc.frequency.exponentialRampToValueAtTime(800, now + 0.15);
  250. gain.gain.setValueAtTime(0.1, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
  251. osc.start(now); osc.stop(now + 0.15);
  252. } else if (type === 'error') {
  253. osc.type = 'sawtooth'; osc.frequency.setValueAtTime(150, now);
  254. gain.gain.setValueAtTime(0.1, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
  255. osc.start(now); osc.stop(now + 0.2);
  256. }
  257. }
  258. function playSliderSound(value, max, min = 0, type = 'high') {
  259. if (!uiAudioCtx) return;
  260. const osc = uiAudioCtx.createOscillator(), gain = uiAudioCtx.createGain();
  261. osc.connect(gain); gain.connect(uiAudioCtx.destination);
  262. let freq = type === 'high' ? 300 + ((value - min) / (max - min)) * 1000 : 150 + ((value - min) / (max - min)) * 300;
  263. osc.type = 'sine'; let now = uiAudioCtx.currentTime;
  264. osc.frequency.setValueAtTime(freq, now);
  265. gain.gain.setValueAtTime(0.05, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
  266. osc.start(now); osc.stop(now + 0.05);
  267. }
  268. function changeTheme(themeName) { document.documentElement.setAttribute('data-theme', themeName); playUISound('action'); }
  269. function changeFilter(filterValue) { document.getElementById('bg-layer').style.filter = filterValue; playUISound('click'); }
  270. function toggleFullscreen() {
  271. if (!document.fullscreenElement) document.documentElement.requestFullscreen();
  272. else if (document.exitFullscreen) document.exitFullscreen();
  273. }
  274. function downloadGraph(canvasId) {
  275. const canvas = document.getElementById(canvasId);
  276. const link = document.createElement('a');
  277. link.download = 'HarmoniX_Graph.png';
  278. // Фикс черного фона при скачивании PNG
  279. const tempCanvas = document.createElement('canvas');
  280. tempCanvas.width = canvas.width; tempCanvas.height = canvas.height;
  281. const tCtx = tempCanvas.getContext('2d');
  282. tCtx.fillStyle = '#ffffff'; tCtx.fillRect(0,0, tempCanvas.width, tempCanvas.height);
  283. tCtx.drawImage(canvas, 0, 0);
  284. link.href = tempCanvas.toDataURL();
  285. link.click();
  286. playUISound('action');
  287. }
  288. function switchTab(index) {
  289. currentWave = index;
  290. document.querySelectorAll('.tab').forEach((el, i) => el.classList.toggle('active', i === index));
  291. document.getElementById('freq').value = waves[index].f; document.getElementById('amp').value = waves[index].a;
  292. updateLabels(); playUISound('click');
  293. }
  294. function updateStep(step) { freqStep = step; document.getElementById('freq').step = step; }
  295. function updateLabels() { document.getElementById('valV').innerText = waves[currentWave].f; document.getElementById('valA').innerText = waves[currentWave].a; }
  296. function updateWave() { waves[currentWave].f = parseInt(document.getElementById('freq').value); waves[currentWave].a = parseInt(document.getElementById('amp').value); updateLabels(); updateAudio(); }
  297. function toggleClip() {
  298. isClipped = !isClipped;
  299. let btn = document.getElementById('btnClip');
  300. if (isClipped) { btn.classList.add('active'); btn.innerText = "⚡ Убрать искажение"; }
  301. else { btn.classList.remove('active'); btn.innerText = "⚡ Клиппинг (Искажение)"; }
  302. updateAudio();
  303. }
  304. function resetAll() {
  305. waves = [{ f: 440, a: 80 }, { f: 880, a: 0 }, { f: 1320, a: 0 }];
  306. isClipped = false;
  307. let btnClip = document.getElementById('btnClip'); btnClip.classList.remove('active'); btnClip.innerText = "⚡ Клиппинг (Искажение)";
  308. changeTheme('default'); document.getElementById('themeSelect').value = 'default';
  309. document.getElementById('bg-layer').style.backgroundImage = 'none';
  310. document.getElementById('filterSelect').value = 'none'; changeFilter('none');
  311. switchTab(0); updateAudio();
  312. }
  313. function toggleAudio() {
  314. if (!audioCtx) {
  315. audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  316. for (let i = 0; i < 3; i++) {
  317. let osc = audioCtx.createOscillator(), gain = audioCtx.createGain();
  318. osc.type = 'sine'; osc.connect(gain); gain.connect(audioCtx.destination);
  319. osc.start(); oscillators.push({ osc: osc, gain: gain });
  320. }
  321. }
  322. isAudioOn = !isAudioOn;
  323. let btn = document.getElementById('btnAudio');
  324. if (isAudioOn) { audioCtx.resume(); btn.classList.add('active'); btn.innerText = "🔇 Выкл. Динамик"; }
  325. else { audioCtx.suspend(); btn.classList.remove('active'); btn.innerText = "🔊 Динамик"; }
  326. updateAudio();
  327. }
  328. function updateAudio() {
  329. if (!audioCtx) return;
  330. for (let i = 0; i < 3; i++) {
  331. oscillators[i].osc.frequency.setValueAtTime(waves[i].f, audioCtx.currentTime);
  332. let vol = isAudioOn ? (waves[i].a * 0.005) : 0;
  333. if (isClipped && isAudioOn && waves[i].a > 0) vol *= 2.5;
  334. oscillators[i].gain.gain.setTargetAtTime(vol, audioCtx.currentTime, 0.05);
  335. oscillators[i].osc.type = isClipped ? 'square' : 'sine';
  336. }
  337. }
  338. function draw() {
  339. time += 0.08;
  340. let baseColor = getComputedStyle(document.documentElement).getPropertyValue('--base-wave').trim();
  341. let sumColor = "#E91E63";
  342. let gridColor = "rgba(128,128,128,0.3)";
  343. let c1 = document.getElementById('canvas-single'), ctx1 = c1.getContext('2d');
  344. ctx1.clearRect(0, 0, c1.width, c1.height);
  345. ctx1.beginPath(); ctx1.strokeStyle = gridColor; ctx1.moveTo(0, c1.height/2); ctx1.lineTo(c1.width, c1.height/2); ctx1.stroke();
  346. ctx1.beginPath(); ctx1.strokeStyle = baseColor; ctx1.lineWidth = 2;
  347. for (let x = 0; x < c1.width; x+=2) {
  348. let visualF = (waves[currentWave].f / 200) || 1;
  349. let y = Math.cos(x * 0.02 * visualF - time) * (waves[currentWave].a / 100) * (c1.height/2.5);
  350. if(x===0) ctx1.moveTo(x, c1.height/2 - y); else ctx1.lineTo(x, c1.height/2 - y);
  351. }
  352. ctx1.stroke();
  353. let c2 = document.getElementById('canvas-sum'), ctx2 = c2.getContext('2d');
  354. ctx2.clearRect(0, 0, c2.width, c2.height);
  355. ctx2.beginPath(); ctx2.strokeStyle = gridColor; ctx2.moveTo(0, c2.height/2); ctx2.lineTo(c2.width, c2.height/2); ctx2.stroke();
  356. ctx2.beginPath(); ctx2.strokeStyle = sumColor; ctx2.lineWidth = 2;
  357. for (let x = 0; x < c2.width; x+=2) {
  358. let sumY = 0;
  359. for (let i = 0; i < 3; i++) {
  360. let visualF = (waves[i].f / 200) || 1;
  361. sumY += Math.cos(x * 0.02 * visualF - time) * (waves[i].a / 100) * (c2.height/2.5);
  362. }
  363. if (isClipped) {
  364. let limit = c2.height / 4;
  365. if (sumY > limit) sumY = limit;
  366. if (sumY < -limit) sumY = -limit;
  367. }
  368. if(x===0) ctx2.moveTo(x, c2.height/2 - sumY/1.5); else ctx2.lineTo(x, c2.height/2 - sumY/1.5);
  369. }
  370. ctx2.stroke();
  371. let c3 = document.getElementById('canvas-spectrum'), ctx3 = c3.getContext('2d');
  372. ctx3.clearRect(0, 0, c3.width, c3.height);
  373. ctx3.beginPath(); ctx3.strokeStyle = gridColor; ctx3.moveTo(0, c3.height - 15); ctx3.lineTo(c3.width, c3.height - 15); ctx3.stroke();
  374. ctx3.fillStyle = "rgba(128,128,128,0.8)"; ctx3.font = "10px Arial";
  375. ctx3.fillText("5кГц", (5000 / 20000) * c3.width, c3.height - 2);
  376. ctx3.fillText("10кГц", (10000 / 20000) * c3.width, c3.height - 2);
  377. ctx3.fillText("15кГц", (15000 / 20000) * c3.width, c3.height - 2);
  378. ctx3.fillStyle = baseColor;
  379. for (let i = 0; i < 3; i++) {
  380. if (waves[i].a > 0) {
  381. let xPos = (waves[i].f / 20000) * c3.width;
  382. let h = (waves[i].a / 100) * (c3.height - 30);
  383. ctx3.fillRect(Math.max(xPos, 5), c3.height - 15 - h, 10, h);
  384. }
  385. }
  386. if (isClipped && waves[0].a > 0) {
  387. ctx3.fillStyle = "rgba(233, 30, 99, 0.6)";
  388. for (let i = 3; i <= 9; i += 2) {
  389. let fakeF = waves[0].f * i;
  390. if (fakeF > 20000) break;
  391. let xPos = (fakeF / 20000) * c3.width;
  392. let h = (waves[0].a / (i*2) / 100) * (c3.height - 30);
  393. ctx3.fillRect(Math.max(xPos, 5), c3.height - 15 - h, 6, h);
  394. }
  395. }
  396. animationFrameId = requestAnimationFrame(draw);
  397. }
  398. updateLabels();
  399. </script>
  400. </body>
  401. </html>