| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- <!DOCTYPE html>
- <html lang="ru">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Механические волны и Фурье v1.2</title>
- <style>
- /* CSS-переменные для движка тем */
- :root {
- --bg-color: #fdfaf6;
- --bg-image: none;
- --bg-filter: none;
- --panel-bg: rgba(255, 255, 255, 0.95);
- --text-color: #333;
- --border-color: #ccc;
- --accent: #4CAF50;
- --btn-glow: rgba(76, 175, 80, 0.6);
- --canvas-bg: #fafafa;
- }
- [data-theme="dark"] {
- --bg-color: #121212;
- --panel-bg: rgba(30, 30, 30, 0.95);
- --text-color: #eee;
- --border-color: #444;
- --accent: #bb86fc;
- --btn-glow: rgba(187, 134, 252, 0.6);
- --canvas-bg: #2c2c2c;
- }
- [data-theme="neon"] {
- --bg-color: #000016;
- --panel-bg: rgba(10, 10, 30, 0.85);
- --text-color: #0ff;
- --border-color: #f0f;
- --accent: #f0f;
- --btn-glow: rgba(255, 0, 255, 0.8);
- --canvas-bg: #000;
- }
- body {
- font-family: Arial, sans-serif;
- background-color: var(--bg-color);
- background-image: var(--bg-image);
- background-size: cover;
- background-position: center;
- background-attachment: fixed;
- margin: 0;
- padding: 10px;
- color: var(--text-color);
- transition: all 0.3s ease;
- }
-
- /* Слой для фильтров фона */
- .bg-overlay {
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
- backdrop-filter: var(--bg-filter);
- z-index: -1;
- pointer-events: none;
- }
- /* Верхнее меню настроек */
- .top-bar {
- position: absolute; top: 10px; right: 20px;
- background: var(--panel-bg); padding: 10px 15px;
- border: 1px solid var(--border-color); border-radius: 8px;
- box-shadow: 0 0 10px var(--btn-glow);
- display: flex; gap: 10px; align-items: center; z-index: 100;
- }
- .top-bar select, .top-bar input[type="file"] {
- background: var(--bg-color); color: var(--text-color);
- border: 1px solid var(--border-color); padding: 5px; border-radius: 4px;
- }
- .grid-container {
- display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: auto auto;
- gap: 15px; height: 90vh; margin-top: 60px;
- }
- .panel {
- background: var(--panel-bg); border: 1px solid var(--border-color);
- padding: 15px; border-radius: 8px;
- box-shadow: 0 4px 15px rgba(0,0,0,0.2);
- display: flex; flex-direction: column;
- backdrop-filter: blur(5px);
- }
- canvas {
- width: 100%; height: 200px;
- background: var(--canvas-bg); border: 1px solid var(--border-color);
- margin-top: 10px; border-radius: 4px;
- }
- .formulas {
- font-family: monospace; background: rgba(0,0,0,0.05);
- padding: 10px; text-align: center; font-weight: bold;
- border: 1px dashed var(--accent); margin-bottom: 10px;
- }
- .tabs { display: flex; gap: 5px; margin-bottom: 15px; }
- .tab {
- padding: 8px 15px; cursor: pointer; background: transparent;
- border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-color);
- transition: all 0.2s;
- }
- .tab.active {
- background: var(--accent); color: #fff; font-weight: bold;
- border-color: var(--accent); box-shadow: 0 0 10px var(--accent);
- }
- .control-row { margin-bottom: 15px; display: flex; align-items: center; }
- label { display: inline-block; width: 140px; font-weight: bold; }
- input[type=range] { flex-grow: 1; margin-right: 15px; cursor: pointer; }
- .value-display { width: 60px; text-align: right; }
-
- .btn {
- padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer;
- font-weight: bold; color: white; margin-right: 10px; margin-bottom: 10px;
- transition: all 0.2s;
- }
- .btn:hover { box-shadow: 0 0 12px var(--btn-glow); transform: translateY(-1px); }
- .btn:active { transform: translateY(1px); }
-
- .btn-audio { background: #2196F3; }
- .btn-dist { background: #9C27B0; }
- .btn-reset { background: #607D8B; }
- .btn-audio.active { background: #f44336; box-shadow: 0 0 15px rgba(244, 67, 54, 0.8); }
- .btn-dist.active { background: #d32f2f; box-shadow: 0 0 15px rgba(211, 47, 47, 0.8); }
- </style>
- </head>
- <body>
- <div class="bg-overlay" id="bgOverlay"></div>
- <!-- Новое меню кастомизации -->
- <div class="top-bar" onmouseenter="playUISound('hover')">
- <label style="width:auto; margin-right:5px;">🎨 Тема:</label>
- <select id="themeSelect" onchange="changeTheme(this.value)">
- <option value="default">Светлая (Базовая)</option>
- <option value="dark">Темная</option>
- <option value="neon">Неон (Киберпанк)</option>
- </select>
-
- <label style="width:auto; margin-left:10px; margin-right:5px;">🖼️ Фон:</label>
- <input type="file" id="bgUpload" accept="image/*" onchange="uploadBackground(event)">
-
- <select id="filterSelect" onchange="changeFilter(this.value)">
- <option value="none">Без фильтра</option>
- <option value="blur(5px)">Размытие</option>
- <option value="grayscale(100%)">Ч/Б</option>
- <option value="sepia(80%)">Сепия</option>
- <option value="invert(100%)">Инверсия</option>
- </select>
- </div>
- <div class="grid-container">
- <div class="panel">
- <h3 style="margin:0;">1. Базовое колебание (выбранное)</h3>
- <canvas id="canvas-single"></canvas>
- </div>
- <div class="panel">
- <h3 style="margin:0;">3. Теорема Фурье (Сумма колебаний)</h3>
- <p style="font-size: 0.85em; font-style: italic; opacity: 0.8; margin: 5px 0 0 0;">
- Любое колебание может быть представлено в виде суммы гармонических колебаний.
- </p>
- <canvas id="canvas-sum"></canvas>
- </div>
- <div class="panel">
- <h3 style="margin:0 0 10px 0;">2. Элементы управления</h3>
- <div class="formulas">
- y = A · cos(wt + θ) | V = 1/T | w = 2πV
- </div>
- <div class="tabs">
- <div class="tab active" onclick="switchTab(0)">V1 (Волна 1)</div>
- <div class="tab" onclick="switchTab(1)">V2 (Волна 2)</div>
- <div class="tab" onclick="switchTab(2)">V3 (Волна 3)</div>
- </div>
- <div class="control-row">
- <label>Шаг частоты:</label>
- <input type="radio" name="step" value="10" onchange="updateStep(10); playUISound('click')"> 10 Гц
- <input type="radio" name="step" value="100" onchange="updateStep(100); playUISound('click')" checked> 100 Гц
- </div>
-
- <div class="control-row">
- <label>Частота V:</label>
- <input type="range" id="freq" min="0" max="20000" step="100" value="440" oninput="updateWave(); playSliderSound(this.value, 20000)">
- <div class="value-display"><span id="valV">440</span> Гц</div>
- </div>
-
- <div class="control-row">
- <label>Амплитуда A:</label>
- <input type="range" id="amp" min="0" max="100" value="80" oninput="updateWave(); playSliderSound(this.value, 100, 0, 'low')">
- <div class="value-display"><span id="valA">80</span>%</div>
- </div>
- <div style="margin-top: 10px;">
- <button class="btn btn-audio" onclick="toggleAudio(); playUISound('action')" id="btnAudio">🔊 Вывод на динамике</button>
- <button class="btn btn-dist" onclick="toggleClip(); playUISound('action')" id="btnClip">Искажение (Клиппинг)</button>
- <button class="btn btn-reset" onclick="resetAll(); playUISound('error')">🔄 Сброс</button>
- </div>
- </div>
- <div class="panel">
- <h3 style="margin:0;">4. Спектр</h3>
- <canvas id="canvas-spectrum"></canvas>
- </div>
- </div>
- <script>
- // --- ДВИЖОК ЗВУКОВ ИНТЕРФЕЙСА (UI Audio) ---
- let uiAudioCtx = null;
-
- function initUIAudio() {
- if (!uiAudioCtx) uiAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
- if (uiAudioCtx.state === 'suspended') uiAudioCtx.resume();
- }
- // Звуки для кнопок
- function playUISound(type) {
- initUIAudio();
- const osc = uiAudioCtx.createOscillator();
- const gain = uiAudioCtx.createGain();
- osc.connect(gain);
- gain.connect(uiAudioCtx.destination);
-
- let now = uiAudioCtx.currentTime;
- if (type === 'click' || type === 'hover') {
- osc.type = 'sine';
- osc.frequency.setValueAtTime(type === 'hover' ? 600 : 800, now);
- gain.gain.setValueAtTime(0.05, now);
- gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
- osc.start(now); osc.stop(now + 0.1);
- } else if (type === 'action') {
- osc.type = 'triangle';
- osc.frequency.setValueAtTime(400, now);
- osc.frequency.exponentialRampToValueAtTime(800, now + 0.15);
- gain.gain.setValueAtTime(0.1, now);
- gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
- osc.start(now); osc.stop(now + 0.15);
- } else if (type === 'error') {
- osc.type = 'sawtooth';
- osc.frequency.setValueAtTime(150, now);
- gain.gain.setValueAtTime(0.1, now);
- gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
- osc.start(now); osc.stop(now + 0.2);
- }
- }
- // Звуки для ползунков (тональность зависит от значения)
- function playSliderSound(value, max, min = 0, type = 'high') {
- initUIAudio();
- const osc = uiAudioCtx.createOscillator();
- const gain = uiAudioCtx.createGain();
- osc.connect(gain);
- gain.connect(uiAudioCtx.destination);
-
- // Вычисляем частоту звука UI на основе значения ползунка
- let freq = type === 'high' ?
- 300 + ((value - min) / (max - min)) * 1000 :
- 150 + ((value - min) / (max - min)) * 300;
-
- osc.type = 'sine';
- let now = uiAudioCtx.currentTime;
- osc.frequency.setValueAtTime(freq, now);
- gain.gain.setValueAtTime(0.05, now);
- gain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
- osc.start(now); osc.stop(now + 0.05);
- }
- // --- ДВИЖОК ТЕМ И ФОНОВ ---
- function changeTheme(themeName) {
- document.documentElement.setAttribute('data-theme', themeName);
- playUISound('action');
- }
- function uploadBackground(event) {
- const file = event.target.files[0];
- if (file) {
- const reader = new FileReader();
- reader.onload = function(e) {
- document.documentElement.style.setProperty('--bg-image', `url(${e.target.result})`);
- playUISound('action');
- };
- reader.readAsDataURL(file);
- }
- }
- function changeFilter(filterValue) {
- document.documentElement.style.setProperty('--bg-filter', filterValue);
- playUISound('click');
- }
- // --- ФИЗИЧЕСКАЯ МОДЕЛЬ (Из предыдущих версий) ---
- let waves = [{ f: 440, a: 80 }, { f: 880, a: 0 }, { f: 1320, a: 0 }];
- let currentWave = 0;
- let audioCtx = null;
- let oscillators = [];
- let isAudioOn = false;
- let isClipped = false;
- let freqStep = 100;
- let time = 0;
- function switchTab(index) {
- currentWave = index;
- document.querySelectorAll('.tab').forEach((el, i) => {
- el.classList.toggle('active', i === index);
- });
- document.getElementById('freq').value = waves[index].f;
- document.getElementById('amp').value = waves[index].a;
- updateLabels();
- playUISound('click');
- }
- function updateStep(step) { freqStep = step; document.getElementById('freq').step = step; }
-
- function updateLabels() {
- document.getElementById('valV').innerText = waves[currentWave].f;
- document.getElementById('valA').innerText = waves[currentWave].a;
- }
- function updateWave() {
- waves[currentWave].f = parseInt(document.getElementById('freq').value);
- waves[currentWave].a = parseInt(document.getElementById('amp').value);
- updateLabels();
- updateAudio();
- }
- function toggleClip() {
- isClipped = !isClipped;
- let btn = document.getElementById('btnClip');
- if (isClipped) {
- btn.classList.add('active'); btn.innerText = "Убрать искажение";
- } else {
- btn.classList.remove('active'); btn.innerText = "Искажение (Клиппинг)";
- }
- updateAudio();
- }
- function resetAll() {
- waves = [{ f: 440, a: 80 }, { f: 880, a: 0 }, { f: 1320, a: 0 }];
- isClipped = false;
- let btnClip = document.getElementById('btnClip');
- btnClip.classList.remove('active'); btnClip.innerText = "Искажение (Клиппинг)";
-
- // Сброс тем
- changeTheme('default');
- document.getElementById('themeSelect').value = 'default';
- document.documentElement.style.setProperty('--bg-image', 'none');
- document.getElementById('filterSelect').value = 'none';
- changeFilter('none');
- switchTab(0); updateAudio();
- }
- function toggleAudio() {
- if (!audioCtx) {
- audioCtx = new (window.AudioContext || window.webkitAudioContext)();
- for (let i = 0; i < 3; i++) {
- let osc = audioCtx.createOscillator();
- let gain = audioCtx.createGain();
- osc.type = 'sine';
- osc.connect(gain);
- gain.connect(audioCtx.destination);
- osc.start();
- oscillators.push({ osc: osc, gain: gain });
- }
- }
-
- isAudioOn = !isAudioOn;
- let btn = document.getElementById('btnAudio');
- if (isAudioOn) {
- audioCtx.resume(); btn.classList.add('active'); btn.innerText = "🔇 Выключить звук";
- } else {
- audioCtx.suspend(); btn.classList.remove('active'); btn.innerText = "🔊 Вывод на динамике";
- }
- updateAudio();
- }
- function updateAudio() {
- if (!audioCtx) return;
- for (let i = 0; i < 3; i++) {
- oscillators[i].osc.frequency.setValueAtTime(waves[i].f, audioCtx.currentTime);
- let vol = isAudioOn ? (waves[i].a * 0.005) : 0;
- if (isClipped && isAudioOn && waves[i].a > 0) vol *= 2.5;
- oscillators[i].gain.gain.setTargetAtTime(vol, audioCtx.currentTime, 0.05);
- oscillators[i].osc.type = isClipped ? 'square' : 'sine';
- }
- }
- function draw() {
- time += 0.08;
-
- let c1 = document.getElementById('canvas-single');
- let ctx1 = c1.getContext('2d');
- ctx1.clearRect(0, 0, c1.width, c1.height);
- ctx1.beginPath(); ctx1.strokeStyle = "rgba(128,128,128,0.5)";
- ctx1.moveTo(0, c1.height/2); ctx1.lineTo(c1.width, c1.height/2); ctx1.stroke();
- // Читаем цвет акцента из темы для графиков
- let accentColor = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim();
- ctx1.beginPath(); ctx1.strokeStyle = accentColor; ctx1.lineWidth = 2;
- for (let x = 0; x < c1.width; x++) {
- let visualF = (waves[currentWave].f / 200) || 1;
- let y = Math.cos(x * 0.02 * visualF - time) * (waves[currentWave].a / 100) * (c1.height/2.5);
- if(x===0) ctx1.moveTo(x, c1.height/2 - y); else ctx1.lineTo(x, c1.height/2 - y);
- }
- ctx1.stroke();
- let c2 = document.getElementById('canvas-sum');
- let ctx2 = c2.getContext('2d');
- ctx2.clearRect(0, 0, c2.width, c2.height);
- ctx2.beginPath(); ctx2.strokeStyle = "rgba(128,128,128,0.5)";
- ctx2.moveTo(0, c2.height/2); ctx2.lineTo(c2.width, c2.height/2); ctx2.stroke();
- ctx2.beginPath(); ctx2.strokeStyle = "#E91E63"; ctx2.lineWidth = 2;
- for (let x = 0; x < c2.width; x++) {
- let sumY = 0;
- for (let i = 0; i < 3; i++) {
- let visualF = (waves[i].f / 200) || 1;
- sumY += Math.cos(x * 0.02 * visualF - time) * (waves[i].a / 100) * (c2.height/2.5);
- }
- if (isClipped) {
- let limit = (c2.height/4);
- if (sumY > limit) sumY = limit; if (sumY < -limit) sumY = -limit;
- }
- if(x===0) ctx2.moveTo(x, c2.height/2 - sumY/1.5); else ctx2.lineTo(x, c2.height/2 - sumY/1.5);
- }
- ctx2.stroke();
- let c3 = document.getElementById('canvas-spectrum');
- let ctx3 = c3.getContext('2d');
- ctx3.clearRect(0, 0, c3.width, c3.height);
- ctx3.beginPath(); ctx3.strokeStyle = "rgba(128,128,128,0.5)";
- ctx3.moveTo(0, c3.height - 15); ctx3.lineTo(c3.width, c3.height - 15); ctx3.stroke();
- ctx3.fillStyle = "rgba(128,128,128,0.8)"; ctx3.font = "10px Arial";
- ctx3.fillText("0", 5, c3.height - 2);
- ctx3.fillText("5кГц", (5000 / 20000) * c3.width, c3.height - 2);
- ctx3.fillText("10кГц", (10000 / 20000) * c3.width, c3.height - 2);
- ctx3.fillText("15кГц", (15000 / 20000) * c3.width, c3.height - 2);
- ctx3.fillStyle = accentColor;
- for (let i = 0; i < 3; i++) {
- if (waves[i].a > 0) {
- let xPos = (waves[i].f / 20000) * c3.width;
- let h = (waves[i].a / 100) * (c3.height - 30);
- ctx3.fillRect(Math.max(xPos, 5), c3.height - 15 - h, 10, h);
- }
- }
- if (isClipped && waves[0].a > 0) {
- ctx3.fillStyle = "rgba(233, 30, 99, 0.6)";
- for (let i = 3; i <= 9; i += 2) {
- let fakeF = waves[0].f * i;
- if (fakeF > 20000) break;
- let xPos = (fakeF / 20000) * c3.width;
- let h = (waves[0].a / (i*2) / 100) * (c3.height - 30);
- ctx3.fillRect(Math.max(xPos, 5), c3.height - 15 - h, 6, h);
- }
- }
- requestAnimationFrame(draw);
- }
- function resizeCanvases() {
- document.querySelectorAll('canvas').forEach(c => {
- c.width = c.parentElement.clientWidth - 30;
- });
- }
-
- window.addEventListener('resize', resizeCanvases);
- resizeCanvases(); draw(); updateLabels();
- </script>
- </body>
- </html>
|