// Константы типов длительностей и количество шагов внутри ОДНОЙ доли const DURATION_STEPS = { '4-Е НОТЫ': 1, '8-Е НОТЫ': 2, '8-Е ТРИОЛИ': 3, '16-Е НОТЫ': 4, 'СЕКСТОЛИ': 6, '32-Е НОТЫ': 8, // Названия из селекторов на скриншоте для точного совпадения '4-е н.': 1, '8-е н.': 2, '8-е тр.': 3, '16-е н.': 4, 'секстоли': 6, '32-е н.': 8 }; /** * Класс управления интерактивной сеткой с динамическими долями */ class DynamicDrumGrid { constructor(options) { this.bpm = options.bpm || 98; this.numMeasures = options.numMeasures || 1; // Настройки долей из интерфейса (как на изображении image_cf7223.png) // Массив строк, соответствующий Доле 1, Доле 2, Доле 3, Доле 4 this.beatSettings = options.beatSettings || ['16-е н.', '16-е н.', 'секстоли', '8-е тр.']; this.instruments = [ 'CRASH1', 'CRASH2', 'RIDEBELL', 'RIDE', 'OPENHAT', 'HAT', 'FOOTHAT', 'TOM1', 'TOM2', 'FLOORTOM', 'SNARE', 'RIMCLICK', 'KICK' ]; // Массив шагов, который будет сгенерирован динамически this.timeline = []; this.gridColumnsCount = 0; // Хранилище состояния ячеек (по умолчанию пустое) // Структура: { 'INSTRUMENT': [true, false, ...] } this.matrix = {}; this.initTimeline(); } /** * Преобразует строковое значение селектора в верхний регистр для надежности */ getStepsForBeat(beatType) { const key = beatType.toLowerCase(); return DURATION_STEPS[key] || 4; // по умолчанию 16-е (4 шага на долю) } /** * Генерация таймлайна на основе выбранных долей */ initTimeline() { this.timeline = []; let totalStepIndex = 0; // Итерируемся по тактам for (let m = 0; m < this.numMeasures; m++) { // В каждом такте ровно 4 четвертные доли for (let b = 0; b < 4; b++) { const beatType = this.beatSettings[b]; const stepsInBeat = this.getStepsForBeat(beatType); // Расчет длительности одного шага внутри этой конкретной доли в секундах // Длительность одной четвертной доли = 60 / BPM const beatDurationSeconds = 60.0 / this.bpm; const stepDurationSeconds = beatDurationSeconds / stepsInBeat; for (let s = 0; s < stepsInBeat; s++) { this.timeline.push({ globalIndex: totalStepIndex, measure: m + 1, beat: b + 1, stepInBeat: s + 1, duration: stepDurationSeconds, beatType: beatType }); totalStepIndex++; } } } this.gridColumnsCount = this.timeline.length; this.resetMatrix(); } /** * Сброс/инициализация матрицы триггеров под новое количество колонок */ resetMatrix() { this.instruments.forEach(inst => { this.matrix[inst] = new Array(this.gridColumnsCount).fill(false); }); } /** * Установка значения в матрицу (для загрузки сохраненных уроков преподавателя) */ setCell(instrument, stepIndex, value) { if (this.matrix[instrument] && stepIndex < this.gridColumnsCount) { this.matrix[instrument][stepIndex] = value; } } /** * Генерация HTML-разметки для панели студента * Создает визуальную сетку, где ширина колонок или их группировка отражает доли */ renderStudentGrid(containerElementId) { const container = document.getElementById(containerElementId); if (!container) return; container.innerHTML = ''; // Создаем таблицу сетки const table = document.createElement('table'); table.className = 'student-drum-grid'; table.style.borderCollapse = 'collapse'; table.style.width = '100%'; table.style.backgroundColor = '#111'; table.style.color = '#fff'; table.style.fontFamily = 'sans-serif'; // 1. Рендерим шапку с долями const headerRow = document.createElement('tr'); const cornerCell = document.createElement('th'); cornerCell.style.border = '1px solid #333'; cornerCell.style.padding = '10px'; headerRow.appendChild(cornerCell); this.timeline.forEach((step) => { const th = document.createElement('th'); th.innerText = `${step.beat}.${step.stepInBeat}`; th.style.border = '1px solid #333'; th.style.fontSize = '10px'; th.style.padding = '5px'; // Визуальное разделение долей цветом (черный/оранжевый стиль) if (step.beat % 2 === 0) { th.style.backgroundColor = '#222'; } else { th.style.backgroundColor = '#1a1a1a'; } // Выделение первой субдоли каждой доли if (step.stepInBeat === 1) { th.style.borderLeft = '2px solid #ff6600'; // Оранжевый маркер начала доли } headerRow.appendChild(th); }); table.appendChild(headerRow); // 2. Рендерим строки инструментов this.instruments.forEach(inst => { const row = document.createElement('tr'); // Название инструмента const labelCell = document.createElement('td'); labelCell.innerText = inst; labelCell.style.border = '1px solid #333'; labelCell.style.padding = '8px'; labelCell.style.fontWeight = 'bold'; labelCell.style.fontSize = '12px'; labelCell.style.width = '100px'; row.appendChild(labelCell); // Ячейки сетки for (let i = 0; i < this.gridColumnsCount; i++) { const cell = document.createElement('td'); cell.style.border = '1px solid #333'; cell.style.height = '30px'; cell.style.cursor = 'default'; const currentStep = this.timeline[i]; // Сетка по долям — визуальные границы if (currentStep.stepInBeat === 1) { cell.style.borderLeft = '2px solid #ff6600'; } if (currentStep.beatType.includes('тр') || currentStep.beatType.includes('секстоли')) { cell.style.backgroundColor = '#151c15'; // Легкий оттенок для триольных сеток } // Если нота активна — красим в фирменный оранжевый if (this.matrix[inst][i]) { cell.style.backgroundColor = '#ff6600'; cell.style.color = '#000'; cell.style.textAlign = 'center'; cell.style.fontWeight = 'bold'; cell.style.fontSize = '12px'; cell.innerText = 'R'; // Из скриншота "R" (Right) } row.appendChild(cell); } table.appendChild(row); }); container.appendChild(table); } /** * Пример воспроизведения с учетом динамических длительностей шагов. * Вместо фиксированного интервала, таймаут или веб-аудио время сдвигается * на величину step.duration текущего шага. */ play(audioContext, sampleBuffers, onStepTrigger) { let currentStepIndex = 0; const totalSteps = this.gridColumnsCount; let nextStepTime = audioContext.currentTime; const scheduler = () => { while (nextStepTime < audioContext.currentTime + 0.1) { const stepData = this.timeline[currentStepIndex]; // Проверяем все инструменты на этом шаге this.instruments.forEach(inst => { if (this.matrix[inst][currentStepIndex]) { // Функция триггера звука (в реальном коде заменить на свой плеер) if (sampleBuffers && sampleBuffers[inst]) { this.playSample(audioContext, sampleBuffers[inst], nextStepTime); } if (onStepTrigger) { onStepTrigger(inst, currentStepIndex, nextStepTime); } } }); // КЛЮЧЕВОЙ МОМЕНТ: Сдвигаем время на индивидуальную длительность текущего шага! nextStepTime += stepData.duration; currentStepIndex++; if (currentStepIndex >= totalSteps) { currentStepIndex = 0; // Зацикливание } } this.timerId = setTimeout(scheduler, 25); }; scheduler(); } playSample(audioContext, buffer, time) { const source = audioContext.createBufferSource(); source.buffer = buffer; source.connect(audioContext.destination); source.start(time); } stop() { if (this.timerId) { clearTimeout(this.timerId); } } } // ========================================== // ПРИМЕР ИНИЦИАЛИЗАЦИИ НА СТУДЕНТСКОЙ ПАНЕЛИ // ========================================== /* const studentGrid = new DynamicDrumGrid({ bpm: 98, numMeasures: 1, beatSettings: ['16-е н.', '16-е н.', 'секстоли', '8-е тр.'] // Данные забираются из конфига сохраненного урока }); // Заполняем данными как на скрине (инструмент HAT, шаги 0 и 4 - первые шестнадцатые первой и второй доли) studentGrid.setCell('HAT', 0, true); studentGrid.setCell('HAT', 4, true); // Начиная с 3-й доли (индекс 8 - начало секстолей) заполняем подряд все оранжевые ячейки for(let i = 8; i < studentGrid.gridColumnsCount; i++) { studentGrid.setCell('HAT', i, true); } // Отрисовка интерфейса студента в контейнере
studentGrid.renderStudentGrid('student-grid-container'); */
Made on
Tilda