| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442 |
- <!DOCTYPE html>
- <html lang="ru">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>HarmoniX | Волновой Спектр</title>
- <style>
- :root {
- --bg-color: #fdfaf6;
- --panel-bg: rgba(255, 255, 255, 0.75);
- --text-color: #333;
- --border-color: rgba(204, 204, 204, 0.5);
- --accent: #4CAF50;
- --base-wave: #2196F3;
- --btn-glow: rgba(76, 175, 80, 0.6);
- --canvas-bg: rgba(250, 250, 250, 0.85);
- }
- [data-theme="dark"] {
- --bg-color: #121212; --panel-bg: rgba(30, 30, 30, 0.75);
- --text-color: #eee; --border-color: rgba(68, 68, 68, 0.5); --accent: #bb86fc;
- --base-wave: #03dac6; --btn-glow: rgba(187, 134, 252, 0.6); --canvas-bg: rgba(44, 44, 44, 0.85);
- }
- [data-theme="neon"] {
- --bg-color: #000016; --panel-bg: rgba(10, 10, 30, 0.75);
- --text-color: #0ff; --border-color: rgba(255, 0, 255, 0.5); --accent: #f0f;
- --base-wave: #0ff; --btn-glow: rgba(255, 0, 255, 0.8); --canvas-bg: rgba(0, 0, 0, 0.85);
- }
- 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); }
-
- #bg-layer {
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
- background-color: var(--bg-color);
- background-size: cover; background-position: center;
- z-index: -2; transition: filter 0.3s;
- }
- /* --- Анимированный градиент для логотипа --- */
- .gradient-text {
- background: linear-gradient(45deg, #ff00cc, #3333ff, #00ffcc, #ff00cc);
- background-size: 300% 300%;
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- animation: gradientFlow 5s ease infinite;
- font-weight: bold;
- }
- @keyframes gradientFlow {
- 0% { background-position: 0% 50%; }
- 50% { background-position: 100% 50%; }
- 100% { background-position: 0% 50%; }
- }
- #welcome-screen {
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
- display: flex; flex-direction: column; justify-content: center; align-items: center;
- background: var(--bg-color); z-index: 1000; transition: opacity 0.5s;
- }
- .logo { font-size: 80px; margin-bottom: 20px; text-shadow: 0 0 30px rgba(0, 212, 255, 0.5); }
- .app-title { font-size: 56px; margin: 0; }
- .app-subtitle { font-size: 20px; color: var(--text-color); margin-bottom: 40px; opacity: 0.8; }
- .btn-enter {
- padding: 15px 40px; font-size: 22px; font-weight: bold; color: #fff;
- background: linear-gradient(45deg, #ff00cc, #3333ff); border: none; border-radius: 30px; cursor: pointer;
- box-shadow: 0 0 20px rgba(51, 51, 255, 0.6); transition: transform 0.2s;
- }
- .btn-enter:hover { transform: scale(1.05); }
- #main-app { display: none; padding: 20px; box-sizing: border-box; height: 100%; }
- .top-bar {
- display: flex; justify-content: space-between; align-items: center;
- background: var(--panel-bg); padding: 10px 20px; border-radius: 8px;
- border: 1px solid var(--border-color); margin-bottom: 15px;
- box-shadow: 0 4px 10px rgba(0,0,0,0.1); backdrop-filter: blur(10px);
- }
- .nav-logo { font-size: 28px; cursor: pointer; display: flex; align-items: center; gap: 10px;}
- .settings-group { display: flex; gap: 15px; align-items: center; flex-wrap: wrap; }
- .settings-group select, .settings-group input[type="file"] {
- background: transparent; 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; min-height: calc(100vh - 120px);
- }
- .panel {
- background: var(--panel-bg); border: 1px solid var(--border-color);
- padding: 15px; border-radius: 8px; display: flex; flex-direction: column;
- backdrop-filter: blur(10px);
- }
- .panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
-
- /* --- Контейнер для ручного изменения размера графиков --- */
- .canvas-wrapper {
- resize: both; /* Разрешаем менять размер вручную */
- overflow: hidden;
- width: 100%;
- height: 200px;
- min-height: 150px;
- min-width: 250px;
- background: var(--canvas-bg);
- border: 1px solid var(--border-color);
- border-radius: 4px;
- position: relative;
- }
- canvas {
- display: block; width: 100%; height: 100%;
- }
-
- .tabs { display: flex; gap: 5px; margin: 10px 0; }
- .tab { padding: 8px 15px; cursor: pointer; background: transparent; border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-color); transition: 0.2s; }
- .tab.active { background: var(--accent); color: #fff; font-weight: bold; border-color: 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; }
-
- .btn { padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; color: white; margin-right: 5px; transition: 0.2s; }
- .btn:hover { filter: brightness(1.1); transform: translateY(-1px); }
- .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); }
- .btn-audio.active { background: #f44336; box-shadow: 0 0 15px rgba(244, 67, 54, 0.6); }
- .btn-dist.active { background: #d32f2f; box-shadow: 0 0 15px rgba(211, 47, 47, 0.6); }
- </style>
- </head>
- <body>
- <div id="bg-layer"></div>
- <div id="welcome-screen">
- <div class="logo">🌊</div>
- <h1 class="app-title gradient-text">HarmoniX</h1>
- <h2 class="app-subtitle">Волновой Спектр / Механические волны и Фурье</h2>
- <button class="btn-enter" onclick="startApp()">Войти в приложение</button>
- </div>
- <div id="main-app">
- <nav class="top-bar">
- <div class="nav-logo gradient-text" onclick="location.reload()"><span>🌊</span> HarmoniX</div>
- <div class="settings-group">
- <label>🎨 Тема:</label>
- <select id="themeSelect" onchange="changeTheme(this.value)">
- <option value="default">Светлая</option>
- <option value="dark">Темная</option>
- <option value="neon">Неон</option>
- </select>
- <label>🖼️ Фон:</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(8px)">Размытие</option>
- <option value="grayscale(100%)">Ч/Б</option>
- <option value="sepia(80%)">Сепия</option>
- <option value="hue-rotate(90deg)">Инверсия цвета</option>
- </select>
- <button class="btn btn-icon" onclick="toggleFullscreen()">🔲 На весь экран</button>
- </div>
- </nav>
- <div class="grid-container">
- <div class="panel">
- <div class="panel-header"><h3 style="margin:0;">1. Базовое колебание</h3><span style="font-size:10px; opacity:0.5;">Потяните за правый нижний угол графика ↘</span></div>
- <div class="canvas-wrapper">
- <canvas id="canvas-single"></canvas>
- </div>
- </div>
- <div class="panel">
- <div class="panel-header">
- <h3 style="margin:0;">3. Теорема Фурье</h3>
- <button class="btn btn-icon" style="padding: 4px 8px; font-size:12px;" onclick="downloadGraph('canvas-sum')">💾 Скачать график</button>
- </div>
- <div class="canvas-wrapper">
- <canvas id="canvas-sum"></canvas>
- </div>
- </div>
- <div class="panel">
- <h3 style="margin:0">2. Управление</h3>
- <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 style="width: 60px; text-align:right"><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 style="width: 60px; text-align:right"><span id="valA">80</span>%</div>
- </div>
- <div>
- <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; margin-bottom: 10px;">4. Спектр</h3>
- <div class="canvas-wrapper">
- <canvas id="canvas-spectrum"></canvas>
- </div>
- </div>
- </div>
- </div>
- <script>
- let waves = [{ f: 440, a: 80 }, { f: 880, a: 0 }, { f: 1320, a: 0 }];
- let currentWave = 0, freqStep = 100, time = 0;
- let isClipped = false, isAudioOn = false;
- let uiAudioCtx = null, audioCtx = null, oscillators = [];
- let animationFrameId = null;
- // --- Умное изменение размеров Canvas (ResizeObserver) ---
- const resizeObserver = new ResizeObserver(entries => {
- for (let entry of entries) {
- const wrapper = entry.target;
- const canvas = wrapper.querySelector('canvas');
- if (canvas) {
- canvas.width = wrapper.clientWidth;
- canvas.height = wrapper.clientHeight;
- }
- }
- });
- function startApp() {
- document.getElementById('welcome-screen').style.opacity = '0';
- setTimeout(() => {
- document.getElementById('welcome-screen').style.display = 'none';
- document.getElementById('main-app').style.display = 'block';
-
- // Подключаем наблюдение за размерами всех оберток графиков
- document.querySelectorAll('.canvas-wrapper').forEach(wrapper => {
- resizeObserver.observe(wrapper);
- });
-
- uiAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
- playUISound('action');
- if (!animationFrameId) draw();
- }, 500);
- }
- function uploadBackground(event) {
- const file = event.target.files[0];
- if (file) {
- const reader = new FileReader();
- reader.onload = function(e) {
- document.getElementById('bg-layer').style.backgroundImage = `url('${e.target.result}')`;
- playUISound('action');
- };
- reader.readAsDataURL(file);
- }
- event.target.value = '';
- }
- function playUISound(type) {
- if (!uiAudioCtx) return;
- const osc = uiAudioCtx.createOscillator(), 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(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') {
- if (!uiAudioCtx) return;
- const osc = uiAudioCtx.createOscillator(), gain = uiAudioCtx.createGain();
- osc.connect(gain); gain.connect(uiAudioCtx.destination);
- 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 changeFilter(filterValue) { document.getElementById('bg-layer').style.filter = filterValue; playUISound('click'); }
- function toggleFullscreen() {
- if (!document.fullscreenElement) document.documentElement.requestFullscreen();
- else if (document.exitFullscreen) document.exitFullscreen();
- }
- function downloadGraph(canvasId) {
- const canvas = document.getElementById(canvasId);
- const link = document.createElement('a');
- link.download = 'HarmoniX_Graph.png';
-
- // Фикс черного фона при скачивании PNG
- const tempCanvas = document.createElement('canvas');
- tempCanvas.width = canvas.width; tempCanvas.height = canvas.height;
- const tCtx = tempCanvas.getContext('2d');
- tCtx.fillStyle = '#ffffff'; tCtx.fillRect(0,0, tempCanvas.width, tempCanvas.height);
- tCtx.drawImage(canvas, 0, 0);
-
- link.href = tempCanvas.toDataURL();
- link.click();
- playUISound('action');
- }
- 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.getElementById('bg-layer').style.backgroundImage = '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(), 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 baseColor = getComputedStyle(document.documentElement).getPropertyValue('--base-wave').trim();
- let sumColor = "#E91E63";
- let gridColor = "rgba(128,128,128,0.3)";
-
- let c1 = document.getElementById('canvas-single'), ctx1 = c1.getContext('2d');
- ctx1.clearRect(0, 0, c1.width, c1.height);
- ctx1.beginPath(); ctx1.strokeStyle = gridColor; ctx1.moveTo(0, c1.height/2); ctx1.lineTo(c1.width, c1.height/2); ctx1.stroke();
- ctx1.beginPath(); ctx1.strokeStyle = baseColor; ctx1.lineWidth = 2;
- for (let x = 0; x < c1.width; x+=2) {
- 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'), ctx2 = c2.getContext('2d');
- ctx2.clearRect(0, 0, c2.width, c2.height);
- ctx2.beginPath(); ctx2.strokeStyle = gridColor; ctx2.moveTo(0, c2.height/2); ctx2.lineTo(c2.width, c2.height/2); ctx2.stroke();
- ctx2.beginPath(); ctx2.strokeStyle = sumColor; ctx2.lineWidth = 2;
- for (let x = 0; x < c2.width; x+=2) {
- 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'), ctx3 = c3.getContext('2d');
- ctx3.clearRect(0, 0, c3.width, c3.height);
- ctx3.beginPath(); ctx3.strokeStyle = gridColor; 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("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 = baseColor;
- 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);
- }
- }
- animationFrameId = requestAnimationFrame(draw);
- }
- updateLabels();
- </script>
- </body>
- </html>
|