и 2 ниже модуля. // 🚀 Модуль экспресс-метода снятия стресса (function() { 'use strict'; const ExpressModule = { name: 'express', displayName: 'Экспресс-метод', icon: '⚡', description: 'Быстрое снятие стресса с помощью звуковой терапии', // Состояние state: { isRunning: false, isPlaying: false, audioContext: null, sourceNode: null, gainNode: null, filterNode: null, audioBuffer: null }, start: function() { console.log('⚡ Запуск экспресс-метода'); const container = MindEaseAPI.getContainer(); container.innerHTML = this.getHTML(); this.initExpressMethod(); }, stop: function() { console.log('🛑 Экспресс-метод остановлен'); this.cleanup(); }, getHTML: function() { return `

Экспресс-метод снятия стресса

Уровень: 5 (1.25x)

Громкость: 50%

Тон: Норма

Градусник
`; }, initExpressMethod: function() { this.state.isRunning = true; this.initThermometerFunctionality(); // Адаптивность для мобильных устройств if (window.innerWidth <= 768) { const container = document.querySelector('.thermometer-container-modal'); if (container) { container.style.flexDirection = 'column'; container.style.width = '95vw'; container.style.gap = '2rem'; const controls = document.querySelector('.thermometer-controls'); if (controls) { controls.style.width = '100%'; controls.style.maxWidth = 'none'; } const visual = document.querySelector('.thermometer-visual'); if (visual) { visual.style.marginLeft = '0'; } } } }, initThermometerFunctionality: function() { const levelSlider = document.getElementById('thermometer-level-slider'); const volumeSlider = document.getElementById('thermometer-volume-slider'); const toneSlider = document.getElementById('thermometer-tone-slider'); const levelText = document.getElementById('thermometer-level-text'); const volumeText = document.getElementById('thermometer-volume-text'); const toneText = document.getElementById('thermometer-tone-text'); const playButton = document.getElementById('thermometer-play-button'); const playIcon = document.getElementById('thermometer-play-icon'); const pauseIcon = document.getElementById('thermometer-pause-icon'); const thermometerLiquid = document.getElementById('thermometer-liquid'); const thermometerLiquidBulb = document.getElementById('thermometer-liquid-bulb'); const errorMessageDiv = document.getElementById('thermometer-error-message'); const thermometerVisual = document.querySelector('.thermometer-visual'); let isDraggingThermometer = false; const colors = ['#0064FF', '#00C896', '#FF9600', '#FF3232']; // Web Audio API const self = this; // Инициализация аудио - используем sound.WAV из media/ async function initAudio() { try { if (!self.state.audioContext) { self.state.audioContext = new (window.AudioContext || window.webkitAudioContext)(); } if (self.state.audioContext.state === 'suspended') { await self.state.audioContext.resume(); } if (self.state.audioBuffer) return true; // ИСПРАВЛЕННЫЙ ПУТЬ: звук из папки media/ const response = await fetch('media/sound.WAV'); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const arrayBuffer = await response.arrayBuffer(); self.state.audioBuffer = await self.state.audioContext.decodeAudioData(arrayBuffer); } catch (error) { console.error("Ошибка инициализации аудио:", error); errorMessageDiv.textContent = "Ошибка загрузки звука. Файл 'media/sound.WAV' не найден."; errorMessageDiv.style.display = 'block'; self.state.isPlaying = false; playIcon.style.display = 'block'; pauseIcon.style.display = 'none'; return false; } return true; } function startAudio() { if (!self.state.audioBuffer) return; self.state.sourceNode = self.state.audioContext.createBufferSource(); self.state.sourceNode.buffer = self.state.audioBuffer; self.state.sourceNode.loop = true; self.state.gainNode = self.state.audioContext.createGain(); self.state.filterNode = self.state.audioContext.createBiquadFilter(); self.state.filterNode.type = 'lowpass'; self.state.sourceNode.connect(self.state.filterNode); self.state.filterNode.connect(self.state.gainNode); self.state.gainNode.connect(self.state.audioContext.destination); updateAudio(levelSlider.value, volumeSlider.value, toneSlider.value); self.state.sourceNode.start(0); } function updateUI(level, volume, tone) { const speedMultiplier = 0.5 + (level * 0.15); levelText.textContent = `Уровень: ${level} (${speedMultiplier.toFixed(1)}x)`; volumeText.textContent = `Громкость: ${Math.round(volume * 100)}%`; let toneLabel; if (tone < 0.1) toneLabel = 'Глухо'; else if (tone > 0.9) toneLabel = 'Звонко'; else toneLabel = 'Норма'; toneText.textContent = `Тон: ${toneLabel}`; const fillHeightPercent = (level / 10) * 80; thermometerLiquid.style.height = `${fillHeightPercent}%`; const fillColor = colors[Math.min(Math.floor(level / 3), 3)]; thermometerLiquid.style.backgroundColor = fillColor; thermometerLiquidBulb.style.backgroundColor = fillColor; } function updateAudio(level, volume, tone) { if (!self.state.isPlaying || !self.state.audioContext || !self.state.sourceNode) return; const speedMultiplier = 0.5 + (level * 0.15); const filterFrequency = 200 + tone * 19800; self.state.sourceNode.playbackRate.setValueAtTime(speedMultiplier, self.state.audioContext.currentTime); self.state.gainNode.gain.setValueAtTime(volume, self.state.audioContext.currentTime); self.state.filterNode.frequency.setValueAtTime(filterFrequency, self.state.audioContext.currentTime); } async function togglePlay() { errorMessageDiv.style.display = 'none'; if (self.state.isPlaying) { if (self.state.sourceNode) { self.state.sourceNode.stop(); self.state.sourceNode.disconnect(); self.state.sourceNode = null; } playIcon.style.display = 'block'; pauseIcon.style.display = 'none'; self.state.isPlaying = false; } else { const audioInitialized = await initAudio(); if (audioInitialized) { startAudio(); playIcon.style.display = 'none'; pauseIcon.style.display = 'block'; self.state.isPlaying = true; } } } // Логика перетаскивания градусника function handleThermometerInteraction(e) { const thermometerRect = thermometerVisual.getBoundingClientRect(); const mouseY = e.touches ? e.touches[0].pageY : e.pageY; // Определяем заполняемую область градусника const bottomFillY = thermometerRect.bottom - 60; const topFillY = thermometerRect.top + 30; const fillableHeight = bottomFillY - topFillY; // Ограничиваем Y-координату в пределах заполняемой области const clampedMouseY = Math.max(topFillY, Math.min(bottomFillY, mouseY)); // Вычисляем уровень const normalizedY = (bottomFillY - clampedMouseY) / fillableHeight; const newLevel = Math.round(normalizedY * 10); levelSlider.value = newLevel; updateUI(newLevel, Number(volumeSlider.value), Number(toneSlider.value)); updateAudio(newLevel, Number(volumeSlider.value), Number(toneSlider.value)); } thermometerVisual.addEventListener('mousedown', (e) => { isDraggingThermometer = true; handleThermometerInteraction(e); }); thermometerVisual.addEventListener('touchstart', (e) => { isDraggingThermometer = true; handleThermometerInteraction(e); }); document.addEventListener('mousemove', (e) => { if (isDraggingThermometer) { handleThermometerInteraction(e); } }); document.addEventListener('touchmove', (e) => { if (isDraggingThermometer) { handleThermometerInteraction(e); } }); document.addEventListener('mouseup', () => { isDraggingThermometer = false; }); document.addEventListener('touchend', () => { isDraggingThermometer = false; }); // Назначаем обработчики для слайдеров playButton.addEventListener('click', togglePlay); levelSlider.addEventListener('input', () => { const level = Number(levelSlider.value); updateUI(level, Number(volumeSlider.value), Number(toneSlider.value)); updateAudio(level, Number(volumeSlider.value), Number(toneSlider.value)); }); volumeSlider.addEventListener('input', () => { const volume = Number(volumeSlider.value); updateUI(Number(levelSlider.value), volume, Number(toneSlider.value)); updateAudio(Number(levelSlider.value), volume, Number(toneSlider.value)); }); toneSlider.addEventListener('input', () => { const tone = Number(toneSlider.value); updateUI(Number(levelSlider.value), Number(volumeSlider.value), tone); updateAudio(Number(levelSlider.value), Number(volumeSlider.value), tone); }); // Начальная отрисовка updateUI(5, 0.5, 0.5); // Стили для слайдеров const sliders = document.querySelectorAll('.slider'); sliders.forEach(slider => { slider.style.webkitAppearance = 'none'; slider.style.width = '100%'; slider.style.height = '8px'; slider.style.background = '#4a5568'; slider.style.borderRadius = '5px'; slider.style.outline = 'none'; slider.style.cursor = 'pointer'; slider.style.transition = 'opacity .2s'; // Стили для ползунка slider.addEventListener('input', function() { const value = (this.value - this.min) / (this.max - this.min) * 100; this.style.background = `linear-gradient(to right, #0064FF 0%, #0064FF ${value}%, #4a5568 ${value}%, #4a5568 100%)`; }); // Инициализация градиента const value = (slider.value - slider.min) / (slider.max - slider.min) * 100; slider.style.background = `linear-gradient(to right, #0064FF 0%, #0064FF ${value}%, #4a5568 ${value}%, #4a5568 100%)`; }); }, cleanup: function() { // Останавливаем аудио if (this.state.sourceNode) { this.state.sourceNode.stop(); this.state.sourceNode.disconnect(); this.state.sourceNode = null; } // Закрываем аудио контекст if (this.state.audioContext && this.state.audioContext.state !== 'closed') { this.state.audioContext.close(); } // Сбрасываем состояние this.state = { isRunning: false, isPlaying: false, audioContext: null, sourceNode: null, gainNode: null, filterNode: null, audioBuffer: null }; } }; // Автоматическая регистрация if (typeof MindEaseAPI !== 'undefined') { MindEaseAPI.registerModule(ExpressModule); } else { console.error('MindEaseAPI не найден'); } })();