plugin v1.0.0
This commit is contained in:
1 parent
12c5b55bce
commit
44fce48bb5
12 files changed
+1316
-635
No files matched your search
+248
@@ -285,6 +285,7 @@
|
||||
<div class="tool-icons">
|
||||
<i class="fa-solid fa-table-cells"></i
|
||||
><i class="fa-solid fa-bars-staggered"></i>
|
||||
<i class="fa-solid fa-music" id="open-piano-roll-btn" title="Abrir Piano Roll"></i>
|
||||
<i
|
||||
class="fa-solid fa-wave-square"
|
||||
id="bounce-pattern-btn"
|
||||
@@ -317,6 +318,39 @@
|
||||
</div>
|
||||
<div id="track-container"></div>
|
||||
</div>
|
||||
<div class="piano-roll-editor" id="piano-roll-editor" style="display: none;">
|
||||
<div class="editor-header">
|
||||
<span>Piano Roll - <span id="piano-roll-instrument-name">Instrumento 1</span></span>
|
||||
<div class="window-controls">
|
||||
<i class="fa-solid fa-xmark" id="close-piano-roll-btn"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="piano-roll-toolbar">
|
||||
<div class="playback-controls">
|
||||
<i class="fa-solid fa-pencil active" title="Draw Tool"></i>
|
||||
<i class="fa-solid fa-eraser" title="Erase Tool"></i>
|
||||
</div>
|
||||
<div class="snap-controls">
|
||||
<label>Snap:</label>
|
||||
<select>
|
||||
<option>1/4</option>
|
||||
<option>1/8</option>
|
||||
<option selected>1/16</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="piano-roll-workspace">
|
||||
<div class="piano-keys-container" id="piano-keys-container">
|
||||
<canvas id="piano-keys-canvas"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="piano-grid-container" id="piano-grid-container">
|
||||
<canvas id="piano-grid-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="audio-editor">
|
||||
<div class="editor-header">
|
||||
<span>Editor de Amostras de Áudio</span>
|
||||
@@ -486,6 +520,220 @@
|
||||
document.body.classList.add("embed-mode");
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
// Importamos o estado global para ler/salvar notas reais
|
||||
import { appState } from "./assets/js/creations/state.js"; // Ajuste o caminho se necessário
|
||||
import { sendAction } from "./assets/js/creations/socket.js";
|
||||
import { renderAll } from "./assets/js/creations/ui.js";
|
||||
import * as Tone from "https://esm.sh/tone";
|
||||
|
||||
// Variáveis de Controle
|
||||
let currentTrackId = null;
|
||||
const CONSTANTS = {
|
||||
NOTE_HEIGHT: 20,
|
||||
KEY_WIDTH: 60,
|
||||
BEAT_WIDTH: 40,
|
||||
TOTAL_KEYS: 84, // 7 oitavas
|
||||
START_NOTE: 24, // C1
|
||||
TICKS_PER_PIXEL: 0 // Calculado dinamicamente
|
||||
};
|
||||
|
||||
const pianoRollEditor = document.getElementById('piano-roll-editor');
|
||||
const keysCanvas = document.getElementById('piano-keys-canvas');
|
||||
const gridCanvas = document.getElementById('piano-grid-canvas');
|
||||
const keysCtx = keysCanvas.getContext('2d');
|
||||
const gridCtx = gridCanvas.getContext('2d');
|
||||
const gridContainer = document.getElementById('piano-grid-container');
|
||||
const keysContainer = document.getElementById('piano-keys-container');
|
||||
|
||||
// Sintetizador de preview
|
||||
const previewSynth = new Tone.PolySynth(Tone.Synth).toDestination();
|
||||
previewSynth.volume.value = -10;
|
||||
|
||||
// --- FUNÇÃO GLOBAL PARA ABRIR O EDITOR ---
|
||||
window.openPianoRoll = function(trackId) {
|
||||
const track = appState.pattern.tracks.find(t => t.id === trackId);
|
||||
if (!track) return;
|
||||
|
||||
currentTrackId = trackId;
|
||||
document.getElementById('piano-roll-instrument-name').textContent = track.name;
|
||||
|
||||
// Mostra o editor
|
||||
pianoRollEditor.style.display = 'flex';
|
||||
|
||||
// Redimensiona e desenha
|
||||
resizeCanvas();
|
||||
|
||||
// Centraliza o scroll vertical (C5)
|
||||
const middleY = (CONSTANTS.TOTAL_KEYS / 2) * CONSTANTS.NOTE_HEIGHT;
|
||||
gridContainer.scrollTop = middleY - 200;
|
||||
};
|
||||
|
||||
// --- DESENHO E LÓGICA ---
|
||||
|
||||
function resizeCanvas() {
|
||||
const totalHeight = CONSTANTS.TOTAL_KEYS * CONSTANTS.NOTE_HEIGHT;
|
||||
// 64 compassos * 192 ticks / (ticks por beat) * largura... simplificando:
|
||||
// Vamos fixar uma largura grande por enquanto
|
||||
const totalWidth = 3000;
|
||||
|
||||
keysCanvas.width = CONSTANTS.KEY_WIDTH;
|
||||
keysCanvas.height = totalHeight;
|
||||
gridCanvas.width = totalWidth;
|
||||
gridCanvas.height = totalHeight;
|
||||
|
||||
// Importante: Sincronizar a conversão de Pixel <-> Tick
|
||||
// 1 Beat = 48 ticks (em 16th) ou 192 ticks por bar?
|
||||
// No seu file.js: ticksPerStep = 12 (1/16).
|
||||
// Então BEAT_WIDTH (40px) = 4 steps = 48 ticks.
|
||||
CONSTANTS.TICKS_PER_PIXEL = 48 / CONSTANTS.BEAT_WIDTH;
|
||||
|
||||
drawKeys();
|
||||
drawGrid();
|
||||
drawNotes();
|
||||
}
|
||||
|
||||
function drawKeys() {
|
||||
keysCtx.clearRect(0, 0, keysCanvas.width, keysCanvas.height);
|
||||
for (let i = 0; i < CONSTANTS.TOTAL_KEYS; i++) {
|
||||
const midiNote = CONSTANTS.START_NOTE + (CONSTANTS.TOTAL_KEYS - 1 - i);
|
||||
const y = i * CONSTANTS.NOTE_HEIGHT;
|
||||
const isBlack = isBlackKey(midiNote);
|
||||
|
||||
keysCtx.fillStyle = isBlack ? '#333' : '#eee';
|
||||
keysCtx.fillRect(0, y, CONSTANTS.KEY_WIDTH, CONSTANTS.NOTE_HEIGHT);
|
||||
keysCtx.strokeStyle = '#555';
|
||||
keysCtx.strokeRect(0, y, CONSTANTS.KEY_WIDTH, CONSTANTS.NOTE_HEIGHT);
|
||||
|
||||
if (midiNote % 12 === 0) {
|
||||
keysCtx.fillStyle = isBlack ? '#fff' : '#333';
|
||||
keysCtx.font = '10px Arial';
|
||||
keysCtx.fillText('C' + (Math.floor(midiNote / 12) - 1), 35, y + 14);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawGrid() {
|
||||
gridCtx.fillStyle = '#292929';
|
||||
gridCtx.fillRect(0, 0, gridCanvas.width, gridCanvas.height);
|
||||
|
||||
// Linhas das notas
|
||||
for (let i = 0; i < CONSTANTS.TOTAL_KEYS; i++) {
|
||||
const midiNote = CONSTANTS.START_NOTE + (CONSTANTS.TOTAL_KEYS - 1 - i);
|
||||
const y = i * CONSTANTS.NOTE_HEIGHT;
|
||||
const isBlack = isBlackKey(midiNote);
|
||||
gridCtx.fillStyle = isBlack ? '#222' : '#2a2a2a';
|
||||
gridCtx.fillRect(0, y, gridCanvas.width, CONSTANTS.NOTE_HEIGHT);
|
||||
gridCtx.strokeStyle = '#333';
|
||||
gridCtx.beginPath(); gridCtx.moveTo(0, y); gridCtx.lineTo(gridCanvas.width, y); gridCtx.stroke();
|
||||
}
|
||||
|
||||
// Linhas verticais (Tempo)
|
||||
for (let x = 0; x < gridCanvas.width; x += CONSTANTS.BEAT_WIDTH) {
|
||||
gridCtx.beginPath();
|
||||
gridCtx.strokeStyle = (x % (CONSTANTS.BEAT_WIDTH * 4) === 0) ? '#666' : '#383838';
|
||||
gridCtx.moveTo(x, 0); gridCtx.lineTo(x, gridCanvas.height); gridCtx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function drawNotes() {
|
||||
if (!currentTrackId) return;
|
||||
const track = appState.pattern.tracks.find(t => t.id === currentTrackId);
|
||||
if (!track) return;
|
||||
|
||||
const pattern = track.patterns[track.activePatternIndex];
|
||||
const notes = pattern.notes || [];
|
||||
|
||||
gridCtx.fillStyle = '#ffbb00'; // Cor Laranja
|
||||
gridCtx.strokeStyle = '#000';
|
||||
|
||||
notes.forEach(note => {
|
||||
// Converter MIDI para Y (Invertido)
|
||||
const keyIndex = (CONSTANTS.TOTAL_KEYS - 1) - (note.key - CONSTANTS.START_NOTE);
|
||||
const y = keyIndex * CONSTANTS.NOTE_HEIGHT;
|
||||
|
||||
// Converter Ticks (pos) para X
|
||||
// Se 48 ticks = BEAT_WIDTH (40px) -> pos / 1.2
|
||||
const x = note.pos / CONSTANTS.TICKS_PER_PIXEL;
|
||||
const width = note.len / CONSTANTS.TICKS_PER_PIXEL;
|
||||
|
||||
gridCtx.fillRect(x + 1, y + 1, width - 2, CONSTANTS.NOTE_HEIGHT - 2);
|
||||
gridCtx.strokeRect(x + 1, y + 1, width - 2, CONSTANTS.NOTE_HEIGHT - 2);
|
||||
});
|
||||
}
|
||||
|
||||
// --- INTERAÇÃO: CRIAR NOTAS ---
|
||||
gridCanvas.addEventListener('mousedown', (e) => {
|
||||
if (!currentTrackId) return;
|
||||
|
||||
const rect = gridCanvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
// 1. Calcular Nota (Y)
|
||||
const keyIndex = Math.floor(y / CONSTANTS.NOTE_HEIGHT);
|
||||
const midiNote = CONSTANTS.START_NOTE + (CONSTANTS.TOTAL_KEYS - 1 - keyIndex);
|
||||
|
||||
// 2. Calcular Posição (X) e Snap
|
||||
// Snap padrão 1/16 = 12 ticks
|
||||
const snapTicks = 12;
|
||||
const rawTicks = x * CONSTANTS.TICKS_PER_PIXEL;
|
||||
const quantizedPos = Math.floor(rawTicks / snapTicks) * snapTicks;
|
||||
|
||||
// 3. Criar Objeto Nota
|
||||
const newNote = {
|
||||
pos: quantizedPos,
|
||||
len: 48, // Duração padrão (1 beat/seminima) - ajuste conforme desejar
|
||||
key: midiNote,
|
||||
vol: 100,
|
||||
pan: 0
|
||||
};
|
||||
|
||||
// 4. Atualizar Estado e Redesenhar
|
||||
const track = appState.pattern.tracks.find(t => t.id === currentTrackId);
|
||||
if (track) {
|
||||
const pattern = track.patterns[track.activePatternIndex];
|
||||
|
||||
// Se não existir array de notas, cria
|
||||
if (!pattern.notes) pattern.notes = [];
|
||||
|
||||
pattern.notes.push(newNote);
|
||||
|
||||
// Toca som
|
||||
const noteName = Tone.Frequency(midiNote, "midi").toNote();
|
||||
previewSynth.triggerAttackRelease(noteName, "8n");
|
||||
|
||||
// Redesenha Piano Roll
|
||||
drawNotes();
|
||||
|
||||
// IMPORTANTE: Atualizar a UI da lista de trilhas (para aparecer a miniatura)
|
||||
// E enviar via Socket para colaboradores
|
||||
renderAll();
|
||||
sendAction({
|
||||
type: "UPDATE_PATTERN_NOTES",
|
||||
trackId: currentTrackId,
|
||||
patternIndex: track.activePatternIndex,
|
||||
notes: pattern.notes
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Scroll Sync
|
||||
gridContainer.addEventListener('scroll', () => {
|
||||
keysContainer.scrollTop = gridContainer.scrollTop;
|
||||
});
|
||||
|
||||
// Fechar
|
||||
document.getElementById('close-piano-roll-btn').addEventListener('click', () => {
|
||||
pianoRollEditor.style.display = 'none';
|
||||
currentTrackId = null;
|
||||
});
|
||||
|
||||
function isBlackKey(note) {
|
||||
const n = note % 12;
|
||||
return (n === 1 || n === 3 || n === 6 || n === 8 || n === 10);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||
|
||||
|
||||
Reference in new issue
Block a user