index.html 20 KB

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