diff --git a/assets/css/style.css b/assets/css/style.css index 56571df..a3489e4 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -10,18 +10,28 @@ --text-dark: #888; --accent-green: #2ecc71; --accent-red: #d9534f; + --background-light: #4a4f57; + --background-lighter: #5c626b; + --border-color-dark: #1a1c1e; + --accent-blue: #3498db; } /* =============================================== */ -/* ESTILOS GLOBAIS E LAYOUT PRINCIPAL +/* ESTILOS GLOBAIS E LAYOUT PRINCIPAL (CORRIGIDO) /* =============================================== */ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; background-color: var(--bg-body); color: var(--text-light); + /* Retornamos ao layout com padding para compatibilidade */ padding-left: 300px; + padding-top: 50px; /* Adiciona espaço para a toolbar fixa */ + box-sizing: border-box; transition: padding-left .3s ease; + height: 100vh; + display: flex; /* Usamos flex no body para o main-content crescer */ + flex-direction: column; } body.sidebar-hidden { @@ -33,7 +43,13 @@ body.knob-dragging { } .main-content { - padding: 2rem; + padding: 1rem; + flex-grow: 1; /* Faz o conteúdo principal ocupar o espaço restante */ + display: flex; + flex-direction: column; + gap: 1rem; + overflow: hidden; /* Evita que o conteúdo transborde */ + height: 100%; /* Garante que o flexbox interno funcione */ } /* =============================================== */ @@ -73,43 +89,13 @@ body.sidebar-hidden .sample-browser { padding: 10px; } -.browser-content ul { - list-style: none; - padding-left: 15px; -} - -.browser-content li { - padding: 5px; - cursor: pointer; - border-radius: 3px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - user-select: none; -} - -.browser-content li:hover { - background-color: var(--bg-editor); -} - -.browser-content li i { - margin-right: 8px; - width: 12px; - color: var(--text-dark); - transition: transform .2s; -} - -.browser-content li.directory > ul { - display: none; -} - -.browser-content li.directory.open > ul { - display: block; -} - -.browser-content li.directory.open > .fa-folder { - transform: rotate(90deg); -} +.browser-content ul { list-style: none; padding-left: 15px; } +.browser-content li { padding: 5px; cursor: pointer; border-radius: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; user-select: none; } +.browser-content li:hover { background-color: var(--bg-editor); } +.browser-content li i { margin-right: 8px; width: 12px; color: var(--text-dark); transition: transform .2s; } +.browser-content li.directory > ul { display: none; } +.browser-content li.directory.open > ul { display: block; } +.browser-content li.directory.open > .fa-folder { transform: rotate(90deg); } #sidebar-toggle { position: fixed; @@ -150,27 +136,90 @@ body.sidebar-hidden #sidebar-toggle { background-color: var(--bg-toolbar); border-bottom: 2px solid var(--border-color); transition: left .3s ease; + height: 50px; /* Altura fixa para o cálculo do padding do body */ + box-sizing: border-box; } body.sidebar-hidden .global-toolbar { left: 0; } +/* =============================================== */ +/* NOVO: EDITOR DE AMOSTRAS DE ÁUDIO (AUDIO EDITOR) +/* =============================================== */ -/* =============================================== */ -/* EDITOR DE BATIDAS (BEAT EDITOR) -/* =============================================== */ -.beat-editor { - background-color: var(--bg-body); +/* O container principal que substitui o .future-panel */ +.audio-editor { + height: 50%; + background-color: var(--bg-editor); border: 1px solid var(--border-color); - width: 100%; - max-width: 900px; - margin: auto; + border-radius: 8px; box-shadow: 0 5px 15px rgba(0, 0, 0, .3); - border-radius: 4px; overflow: hidden; + display: flex; + flex-direction: column; } +/* Container para as faixas de áudio, com scroll vertical */ +#audio-track-container { + overflow-y: auto; + flex-grow: 1; +} + +/* Estilo para cada linha de faixa de áudio */ +.audio-track-lane { + display: flex; + align-items: center; + padding: 8px 10px; + background-color: var(--bg-editor); + border-bottom: 1px solid var(--bg-toolbar); + min-height: 40px; /* Altura mínima para cada faixa */ + box-sizing: border-box; +} + + +/* =============================================== */ +/* ESTILOS DO EDITOR DE ÁUDIO (MARCADORES) +/* =============================================== */ + +/* Wrapper para a visualização do espectrograma, permite scroll horizontal */ +.spectrogram-view-wrapper { + flex-grow: 1; + overflow-x: auto; + overflow-y: hidden; + background-color: #2a2c30; + border-radius: 3px; +} + +/* Garante que o grid possa conter elementos posicionados de forma absoluta */ +.spectrogram-view-grid { + position: relative; + display: inline-block; /* Faz o contêiner se ajustar à largura do canvas */ + height: 100%; +} + +/* Estilo para os números de compasso */ +.bar-marker { + position: absolute; + top: 0; + transform: translateX(-50%); /* Centraliza o número sobre a linha */ + background-color: rgba(0, 0, 0, 0.5); + color: var(--text-dark); + padding: 1px 5px; + font-size: 0.7rem; + border-radius: 3px; + user-select: none; /* Impede que o texto seja selecionado */ + z-index: 5; /* Garante que fique acima da forma de onda mas abaixo da agulha */ +} + +/* Mantém o canvas como block para evitar espaçamentos */ +.waveform-canvas { + display: block; +} + +/* =============================================== */ +/* TOOLBAR DO EDITOR +/* =============================================== */ .editor-header { background-color: var(--bg-toolbar); padding: 4px 10px; @@ -179,590 +228,264 @@ body.sidebar-hidden .global-toolbar { justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); + flex-shrink: 0; } -.window-controls i { - margin-left: 12px; - cursor: pointer; +.window-controls i { margin-left: 12px; cursor: pointer; } + +.editor-toolbar, .editor-header .playback-controls { + display: flex; + align-items: center; + gap: 15px; } .editor-toolbar { background-color: var(--bg-toolbar); padding: 5px 10px; - display: flex; - align-items: center; - gap: 15px; border-bottom: 2px solid var(--border-color); + flex-shrink: 0; } -.editor-toolbar i { - cursor: pointer; - padding: 5px; - border-radius: 3px; +.editor-toolbar i, .editor-header .playback-controls i { + cursor: pointer; + padding: 5px; + border-radius: 3px; + font-size: 1rem; } -.editor-toolbar i.enabled { - background-color: var(--bg-body); - box-shadow: inset 0 0 2px #000; -} +.editor-toolbar i.enabled { background-color: var(--bg-body); box-shadow: inset 0 0 2px #000; } -.pattern-selector { - background-color: var(--bg-body); - padding: 5px 15px; - border: 1px solid var(--border-color); - flex-grow: 1; - font-size: .9rem; - border-radius: 2px; -} +.pattern-manager { display: flex; align-items: center; gap: 10px; } +.pattern-selector { background-color: var(--bg-body); color: var(--text-light); padding: 5px 10px; border: 1px solid var(--border-color); font-size: .9rem; border-radius: 2px; } +.pattern-btn { background: var(--bg-body); border: 1px solid var(--border-color); color: var(--text-light); cursor: pointer; border-radius: 3px; width: 28px; height: 28px; font-size: 1.2rem; } /* =============================================== */ /* FAIXAS (TRACK LANES) E SEQUENCIADOR /* =============================================== */ +#track-container { + overflow-y: auto; + flex-grow: 1; +} + .track-lane { display: flex; align-items: center; padding: 8px 10px; background-color: var(--bg-editor); - border: 2px dashed transparent; - transition: border-color 0.2s; + border-bottom: 1px solid var(--bg-toolbar); + border-left: 2px solid transparent; + border-right: 2px solid transparent; + transition: border-color 0.2s, background-color 0.2s; +} + +.track-lane.active-track { + background-color: #40454d; } .track-lane.drag-over { border-color: var(--accent-green); } -.track-info { - display: flex; - align-items: center; - gap: 8px; - width: 180px; - flex-shrink: 0; -} - -.track-info .fa-gear { - font-size: 1.2rem; - cursor: pointer; -} - -.track-mute { +/* Localize a regra .track-mute e substitua por esta */ +.track-solo-btn { width: 25px; height: 12px; - background-color: var(--accent-green); + background-color: var(--accent-red); /* Cor padrão: vermelho */ border-radius: 6px; cursor: pointer; border: 1px solid var(--border-color); box-shadow: inset 0 0 2px #000; + transition: background-color 0.2s, opacity 0.2s; } -.track-name { - color: var(--accent-red); - font-weight: 700; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; +.track-solo-btn:hover { + opacity: 0.8; } -.track-controls { - display: flex; - gap: 5px; - margin: 0 10px; - padding-left: 10px; - border-left: 1px solid var(--bg-toolbar); - flex-shrink: 0; -} - -.knob-container { - text-align: center; - font-size: .7rem; - color: var(--text-dark); -} - -.knob { - width: 28px; - height: 28px; - background-color: var(--bg-toolbar); - border-radius: 50%; - border: 1px solid var(--border-color); - margin-bottom: 2px; - cursor: grab; - box-shadow: inset 0 0 4px #222; - position: relative; -} - -.knob:active { - cursor: grabbing; -} - -.knob-indicator { - width: 2px; - height: 8px; - background-color: var(--text-light); - position: absolute; - top: 2px; - left: 50%; - transform-origin: bottom center; - transform: translateX(-50%) rotate(0deg); - border-radius: 1px; -} - -.step-sequencer-wrapper { - flex-grow: 1; - overflow-x: auto; - overflow-y: hidden; - padding-bottom: 8px; -} - -.step-sequencer { - display: flex; - gap: 4px; -} - -.step-sequencer-wrapper::-webkit-scrollbar { - height: 8px; -} -.step-sequencer-wrapper::-webkit-scrollbar-track { - background: var(--border-color); - border-radius: 4px; -} -.step-sequencer-wrapper::-webkit-scrollbar-thumb { - background: var(--bg-toolbar); - border-radius: 4px; -} -.step-sequencer-wrapper::-webkit-scrollbar-thumb:hover { - background: #555; -} - -/* (CORREÇÃO) CSS para as marcações de compasso */ -.step-wrapper { - position: relative; -} - -.step-marker { - position: absolute; - top: -16px; - left: 1px; - font-size: .6rem; - color: var(--text-dark); - user-select: none; -} - -.step { - width: 28px; - height: 28px; - background-color: #2a2a2a; - border: 1px solid #4a4a4a; - border-radius: 2px; - cursor: pointer; - transition: background-color .1s, transform 0.1s; - flex-shrink: 0; -} - -.step-dark { - background-color: #1e1e1e; -} - -.step:hover { - background-color: #555; - border-color: #888; -} - -.step.active { +/* Quando solado (ativo), o botão fica verde */ +.track-solo-btn.active { background-color: var(--accent-green); - border: 1px solid #fff; - box-shadow: 0 0 8px var(--accent-green); + opacity: 1; } -.step.playing { - transform: scale(1.1); - box-shadow: inset 0 0 8px rgba(255, 255, 255, 0.8); +.track-info { display: flex; align-items: center; gap: 8px; width: 180px; flex-shrink: 0; } +.track-info .fa-gear { font-size: 1.2rem; cursor: pointer; } +.track-mute { width: 25px; height: 12px; background-color: var(--accent-green); border-radius: 6px; cursor: pointer; border: 1px solid var(--border-color); box-shadow: inset 0 0 2px #000; transition: background-color 0.2s, opacity 0.2s; } +.track-mute:hover { + opacity: 0.8; } +.track-mute.active { + background-color: var(--text-dark); + opacity: 0.7; +} +.track-name { color: var(--accent-red); font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.track-controls { display: flex; gap: 5px; margin: 0 10px; padding-left: 10px; border-left: 1px solid var(--bg-toolbar); flex-shrink: 0; } +.knob-container { text-align: center; font-size: .7rem; color: var(--text-dark); } +.knob { width: 28px; height: 28px; background-color: var(--bg-toolbar); border-radius: 50%; border: 1px solid var(--border-color); margin-bottom: 2px; cursor: grab; box-shadow: inset 0 0 4px #222; position: relative; } +.knob:active { cursor: grabbing; } +.knob-indicator { width: 2px; height: 8px; background-color: var(--text-light); position: absolute; top: 2px; left: 50%; transform-origin: bottom center; transform: translateX(-50%) rotate(0deg); border-radius: 1px; } +.step-sequencer-wrapper { flex-grow: 1; overflow-x: auto; overflow-y: hidden; padding-bottom: 8px; } +.step-sequencer { display: flex; gap: 4px; } +.step-sequencer-wrapper::-webkit-scrollbar { height: 8px; } +.step-sequencer-wrapper::-webkit-scrollbar-track { background: var(--border-color); border-radius: 4px; } +.step-sequencer-wrapper::-webkit-scrollbar-thumb { background: var(--bg-toolbar); border-radius: 4px; } +.step-sequencer-wrapper::-webkit-scrollbar-thumb:hover { background: #555; } +.step-wrapper { position: relative; } +.step-marker { position: absolute; top: -16px; left: 1px; font-size: .6rem; color: var(--text-dark); user-select: none; } +.step { width: 28px; height: 28px; background-color: #2a2a2a; border: 1px solid #4a4a4a; border-radius: 2px; cursor: pointer; transition: background-color .1s, transform 0.1s; flex-shrink: 0; } +.step-dark { background-color: #1e1e1e; } +.step:hover { background-color: #555; border-color: #888; } +.step.active { background-color: var(--accent-green); border: 1px solid #fff; box-shadow: 0 0 8px var(--accent-green); } +.step.playing { transform: scale(1.1); box-shadow: inset 0 0 8px rgba(255, 255, 255, 0.8); } /* =============================================== */ /* CONTROLES E INPUTS /* =============================================== */ -.interactive-input-container { - display: flex; - align-items: center; - justify-content: center; - gap: 4px; -} +.interactive-input-container { display: flex; align-items: center; justify-content: center; gap: 4px; } +.compasso-group { display: flex; align-items: center; gap: 4px; } +.value-input { background: 0 0; border: 0; outline: 0; color: var(--accent-green); font-weight: 700; font-size: 1.4rem; font-family: Courier New, Courier, monospace; text-align: center; padding: 0; width: 55px; } +.compasso-input { width: 25px; } +.compasso-separator { color: var(--accent-green); font-weight: 700; font-size: 1.4rem; font-family: Courier New, Courier, monospace; margin: 0 2px; } +.value-input::-webkit-outer-spin-button, .value-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } +.value-input[type=number] { -moz-appearance: textfield; } +.adjust-btn { background: 0 0; border: 0; color: var(--text-dark); font-size: 1rem; font-weight: 700; cursor: pointer; padding: 0 5px; transition: color .2s; line-height: 1; } +.adjust-btn:hover { color: #fff; } +.control-group { display: flex; align-items: center; gap: 15px; padding: 0 10px; } +.control-group i { font-size: 1.2rem; cursor: pointer; color: var(--text-light); transition: color .2s; } +.control-group i:hover { color: #fff; } +.fa-play, .fa-pause { color: var(--accent-green) !important; } +.divider { width: 1px; height: 25px; background-color: var(--border-color); } +.info-display-group { display: flex; align-items: center; gap: 5px; } +.info-display { background-color: #1a1c1e; padding: 5px 8px; border-radius: 3px; text-align: center; } +.info-display .label { color: var(--text-dark); font-size: .6rem; text-transform: uppercase; } +.spacer { flex-grow: 1; } +#metronome-btn { background: 0 0; border: 1px solid var(--text-dark); color: var(--accent-green); font-family: inherit; font-weight: 700; font-size: .8rem; padding: 5px 10px; border-radius: 3px; cursor: pointer; transition: all .2s; } +#metronome-btn:hover { border-color: var(--text-light); background-color: var(--bg-editor); } +#metronome-btn.active { background-color: var(--accent-green); color: var(--bg-body); border-color: var(--accent-green); } -.compasso-group { - display: flex; - align-items: center; - gap: 4px; -} +.modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); z-index: 2000; display: flex; justify-content: center; align-items: center; padding: 1rem; visibility: hidden; opacity: 0; transition: visibility 0s 0.3s, opacity 0.3s; } +.modal-overlay.visible { visibility: visible; opacity: 1; transition: visibility 0s, opacity 0.3s; } +.modal-content { background-color: var(--bg-body); padding: 1.5rem 2rem; border-radius: 6px; border: 1px solid var(--border-color); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); width: 100%; max-width: 500px; position: relative; display: flex; flex-direction: column; gap: 1.5rem; max-height: 90vh; } +.modal-close { position: absolute; top: 10px; right: 15px; font-size: 1.5rem; color: var(--text-dark); cursor: pointer; border: none; background: none; } +.modal-close:hover { color: var(--text-light); } +.modal-title { margin: 0; padding-bottom: 0.5rem; border-bottom: 1px solid var(--bg-toolbar); color: var(--text-light); text-align: center; flex-shrink: 0; } +.modal-section { margin: 0; } +.modal-section h3 { margin-top: 0; margin-bottom: 0.8rem; font-size: 1rem; color: var(--text-light); } +#server-projects-list { max-height: 250px; overflow-y: auto; background-color: var(--bg-toolbar); border: 1px solid var(--border-color); border-radius: 4px; padding: 0.5rem; min-height: 50px; } +#server-projects-list .project-item { background-color: var(--bg-editor); padding: 10px 15px; border-radius: 4px; margin-bottom: 8px; cursor: pointer; transition: background-color 0.2s, color 0.2s; border: 1px solid transparent; } +#server-projects-list .project-item:last-child { margin-bottom: 0; } +#server-projects-list .project-item:hover { background-color: var(--bg-body); color: #fff; border-color: var(--accent-green); } +.modal-button { background-color: var(--bg-toolbar); color: var(--text-light); border: 1px solid var(--border-color); padding: 0.8rem 1.5rem; border-radius: 4px; cursor: pointer; font-size: 1rem; transition: background-color 0.2s, border-color 0.2s; width: 100%; text-align: center; } +.modal-button:hover { background-color: #4a4f57; border-color: #333; } -.value-input { - background: 0 0; - border: 0; - outline: 0; - color: var(--accent-green); - font-weight: 700; - font-size: 1.4rem; - font-family: Courier New, Courier, monospace; - text-align: center; - padding: 0; - width: 55px; -} - -.compasso-input { - width: 25px; -} - -.compasso-separator { - color: var(--accent-green); - font-weight: 700; - font-size: 1.4rem; - font-family: Courier New, Courier, monospace; - margin: 0 2px; -} - -.value-input::-webkit-outer-spin-button, -.value-input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -.value-input[type=number] { - -moz-appearance: textfield; -} - -.adjust-btn { - background: 0 0; - border: 0; - color: var(--text-dark); - font-size: 1rem; - font-weight: 700; - cursor: pointer; - padding: 0 5px; - transition: color .2s; - line-height: 1; -} - -.adjust-btn:hover { - color: #fff; -} - -.control-group { - display: flex; - align-items: center; - gap: 15px; - padding: 0 10px; -} - -.control-group i { - font-size: 1.2rem; - cursor: pointer; - color: var(--text-light); - transition: color .2s; -} - -.control-group i:hover { - color: #fff; -} - -.fa-play, -.fa-pause { - color: var(--accent-green) !important; -} - -.divider { - width: 1px; - height: 25px; - background-color: var(--border-color); -} - -.info-display-group { - display: flex; - align-items: center; - gap: 5px; -} - -.info-display { - background-color: #1a1c1e; - padding: 5px 8px; - border-radius: 3px; - text-align: center; -} - -.info-display .label { - color: var(--text-dark); - font-size: .6rem; - text-transform: uppercase; -} - -.spacer { - flex-grow: 1; -} - -#metronome-btn { - background: 0 0; - border: 1px solid var(--text-dark); - color: var(--accent-green); - font-family: inherit; - font-weight: 700; - font-size: .8rem; - padding: 5px 10px; - border-radius: 3px; - cursor: pointer; - transition: all .2s; -} - -#metronome-btn:hover { - border-color: var(--text-light); - background-color: var(--bg-editor); -} - -#metronome-btn.active { - background-color: var(--accent-green); - color: var(--bg-body); - border-color: var(--accent-green); -} - -/* =============================================== */ -/* MODAL (CAIXA DE DIÁLOGO) -/* =============================================== */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.7); - z-index: 2000; - display: flex; - justify-content: center; - align-items: center; - padding: 1rem; - visibility: hidden; - opacity: 0; - transition: visibility 0s 0.3s, opacity 0.3s; -} - -.modal-overlay.visible { - visibility: visible; - opacity: 1; - transition: visibility 0s, opacity 0.3s; -} - -.modal-content { - background-color: var(--bg-body); - padding: 1.5rem 2rem; - border-radius: 6px; - border: 1px solid var(--border-color); - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); - width: 100%; - max-width: 500px; - position: relative; - display: flex; - flex-direction: column; - gap: 1.5rem; - max-height: 90vh; -} - -.modal-close { - position: absolute; - top: 10px; - right: 15px; - font-size: 1.5rem; - color: var(--text-dark); - cursor: pointer; - border: none; - background: none; -} - -.modal-close:hover { - color: var(--text-light); -} - -.modal-title { - margin: 0; - padding-bottom: 0.5rem; - border-bottom: 1px solid var(--bg-toolbar); - color: var(--text-light); - text-align: center; - flex-shrink: 0; -} - -.modal-section { - margin: 0; -} - -.modal-section h3 { - margin-top: 0; - margin-bottom: 0.8rem; - font-size: 1rem; - color: var(--text-light); -} - -#server-projects-list { - max-height: 250px; - overflow-y: auto; - background-color: var(--bg-toolbar); - border: 1px solid var(--border-color); - border-radius: 4px; - padding: 0.5rem; - min-height: 50px; -} - -#server-projects-list .project-item { - background-color: var(--bg-editor); - padding: 10px 15px; - border-radius: 4px; - margin-bottom: 8px; - cursor: pointer; - transition: background-color 0.2s, color 0.2s; - border: 1px solid transparent; -} -#server-projects-list .project-item:last-child { - margin-bottom: 0; -} -#server-projects-list .project-item:hover { - background-color: var(--bg-body); - color: #fff; - border-color: var(--accent-green); -} - -.modal-button { - background-color: var(--bg-toolbar); - color: var(--text-light); - border: 1px solid var(--border-color); - padding: 0.8rem 1.5rem; - border-radius: 4px; - cursor: pointer; - font-size: 1rem; - transition: background-color 0.2s, border-color 0.2s; - width: 100%; - text-align: center; -} - -.modal-button:hover { - background-color: #4a4f57; - border-color: #333; -} +.file-menu-container { position: relative; } +.toolbar-btn { background-color: var(--background-light); color: var(--text-light); border: 1px solid var(--border-color); border-radius: 3px; padding: 5px 10px; cursor: pointer; font-family: inherit; font-size: 0.8rem; } +.toolbar-btn:hover { background-color: var(--background-lighter); } +.file-menu-dropdown { position: absolute; top: 100%; left: 0; background-color: var(--background-lighter); border: 1px solid var(--border-color-dark); border-radius: 4px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); min-width: 200px; z-index: 1000; overflow: hidden; display: flex; flex-direction: column; } +.file-menu-dropdown.hidden { display: none; } +.file-menu-dropdown a { color: var(--text-light); padding: 8px 12px; text-decoration: none; display: block; font-size: 0.9rem; } +.file-menu-dropdown a:hover { background-color: var(--accent-blue); color: white; } +.menu-divider { height: 1px; background-color: var(--border-color); margin: 4px 0; } /* =============================================== */ -/* ESTILOS RESPONSIVOS +/* ESTILOS RESPONSIVOS (MELHORADO) /* =============================================== */ +@media (max-width: 1200px) { + .info-display-group { + gap: 2px; + } + .info-display { + padding: 4px 6px; + } + .value-input { + font-size: 1.2rem; + width: 45px; + } + .compasso-input { + width: 20px; + } +} @media (max-width: 992px) { - .main-content { - padding: 1.5rem; + .global-toolbar { + gap: 10px; + flex-wrap: wrap; + height: auto; /* Permite que a toolbar cresça se o conteúdo quebrar linha */ + padding-bottom: 10px; } - .beat-editor { - max-width: 100%; + body { + padding-top: 80px; /* Aumenta o espaço para a toolbar maior */ + } + .info-display-group { + order: 3; /* Move o grupo de informações para o final da toolbar */ + width: 100%; + justify-content: space-around; + } + .spacer { + display: none; } } @media (max-width: 768px) { body { - padding-left: 0 !important; - } - .main-content { - padding: 1rem; + padding-left: 0 !important; } .sample-browser { - transform: translateX(-100%); - width: 280px; + transform: translateX(-100%); + position: fixed; /* Volta a ser fixo para deslizar por cima */ + width: 280px; } body:not(.sidebar-hidden) .sample-browser { - transform: translateX(0); + transform: translateX(0); } #sidebar-toggle { - left: 5px; + left: 5px; + transform: translateX(0); + position: fixed; /* Garante que o botão fique visível */ } .global-toolbar { - left: 0; - padding-left: 45px; + left: 0; + padding-left: 45px; } - .editor-toolbar, - .control-group { - flex-wrap: wrap; - gap: 10px; + .main-content { + padding: 10px; + padding-top: 85px; /* Ajusta o padding para a toolbar fixa */ } - .track-lane { - flex-direction: column; - align-items: stretch; - gap: 15px; - padding: 15px; + .track-lane, .audio-track-lane { + flex-direction: column; + align-items: stretch; + gap: 15px; + padding: 15px; } .track-info, .track-controls { - width: 100%; + width: 100%; } .track-controls { - border-left: none; - padding-left: 0; - justify-content: space-around; + border-left: none; + padding-left: 0; + justify-content: space-around; } .step-sequencer-wrapper { - width: 100%; - } - .modal-content { - max-width: 95vw; - padding: 1rem 1.5rem; - gap: 1rem; + width: 100%; } } -/* --- ESTILOS PARA O MENU ARQUIVO --- */ - -.file-menu-container { - position: relative; /* Essencial para o posicionamento do dropdown */ +.spectrogram-view-wrapper { + position: relative; /* Essencial para o posicionamento absoluto do filho */ + overflow: hidden; /* Garante que a agulha não saia dos limites */ } -.toolbar-btn { - background-color: var(--background-light); - color: var(--text-light); - border: 1px solid var(--border-color); - border-radius: 3px; - padding: 5px 10px; - cursor: pointer; - font-family: inherit; - font-size: 0.8rem; -} - -.toolbar-btn:hover { - background-color: var(--background-lighter); -} - -.file-menu-dropdown { +.playhead { position: absolute; - top: 100%; - left: 0; - background-color: var(--background-lighter); - border: 1px solid var(--border-color-dark); - border-radius: 4px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - min-width: 200px; - z-index: 1000; - overflow: hidden; - display: flex; - flex-direction: column; -} - -.file-menu-dropdown.hidden { - display: none; -} - -.file-menu-dropdown a { - color: var(--text-light); - padding: 8px 12px; - text-decoration: none; - display: block; - font-size: 0.9rem; -} - -.file-menu-dropdown a:hover { - background-color: var(--accent-blue); - color: white; -} - -.menu-divider { - height: 1px; - background-color: var(--border-color); - margin: 4px 0; + top: 0; + left: 0; /* A posição será atualizada via JavaScript */ + width: 2px; + height: 100%; + background-color: var(--accent-red, #e74c3c); /* Use uma cor de destaque */ + z-index: 10; + pointer-events: none; /* Impede que a agulha intercepte cliques do mouse */ + transition: background-color 0.3s; /* Efeito suave ao parar */ } \ No newline at end of file diff --git a/assets/js/creations/audio.js b/assets/js/creations/audio.js index 8e2377a..ce17269 100644 --- a/assets/js/creations/audio.js +++ b/assets/js/creations/audio.js @@ -1,7 +1,8 @@ // js/audio.js import { appState } from "./state.js"; -import { highlightStep } from "./ui.js"; +import { highlightStep, updateAudioEditorUI, updatePlayheadVisual, resetPlayheadVisual } from "./ui.js"; import { getTotalSteps } from "./utils.js"; +import { PIXELS_PER_STEP } from "./config.js"; let audioContext; let mainGainNode; @@ -30,6 +31,7 @@ export function initializeAudioContext() { } } +// ... (funções de master volume/pan, formatTime, metronome, sample player, tick, etc. permanecem iguais)... export function updateMasterVolume(volume) { if (mainGainNode) { mainGainNode.gain.setValueAtTime(volume, audioContext.currentTime); @@ -120,7 +122,6 @@ function tick() { appState.tracks.forEach((track) => { if (!track.patterns || track.patterns.length === 0) return; - // Usa o índice GLOBAL para saber qual pattern tocar, sincronizando com a UI. const activePattern = track.patterns[appState.activePatternIndex]; if (activePattern && activePattern.steps[appState.currentStep] && track.samplePath) { @@ -190,4 +191,104 @@ export function togglePlayback() { appState.currentStep = 0; startPlayback(); } +} + +function animationLoop() { + if (!appState.isAudioEditorPlaying || !audioContext) return; + const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120; + const stepsPerSecond = (bpm / 60) * 4; + const pixelsPerSecond = stepsPerSecond * PIXELS_PER_STEP; + const totalElapsedTime = (audioContext.currentTime - appState.audioEditorStartTime) + appState.audioEditorPlaybackTime; + const newPositionPx = totalElapsedTime * pixelsPerSecond; + const maxDuration = appState.audioTracks.reduce((max, track) => (track.audioBuffer && track.audioBuffer.duration > max) ? track.audioBuffer.duration : max, 0); + if (totalElapsedTime >= maxDuration && maxDuration > 0) { + stopAudioEditorPlayback(); + appState.audioEditorPlaybackTime = 0; + resetPlayheadVisual(); + return; + } + updatePlayheadVisual(newPositionPx); + appState.audioEditorAnimationId = requestAnimationFrame(animationLoop); +} + +// --- LÓGICA DE REPRODUÇÃO ATUALIZADA --- +export function startAudioEditorPlayback() { + if (appState.isAudioEditorPlaying || appState.audioTracks.length === 0) return; + initializeAudioContext(); + + appState.isAudioEditorPlaying = true; + appState.activeAudioSources = []; + updateAudioEditorUI(); + + const startTime = audioContext.currentTime; + appState.audioEditorStartTime = startTime; + + // Verifica se existe alguma faixa no modo "solo" + const isAnyTrackSoloed = appState.audioTracks.some(t => t.isSoloed); + + appState.audioTracks.forEach(track => { + // Condições para tocar: + // 1. A faixa deve ter um buffer de áudio. + // 2. A faixa não pode estar mutada. + const canPlay = track.audioBuffer && !track.isMuted; + + // 3. Lógica de solo: + // - Se houver alguma faixa solada, esta faixa TAMBÉM deve estar solada. + // - Se NENHUMA faixa estiver solada, todas podem tocar. + const shouldPlay = isAnyTrackSoloed ? track.isSoloed : true; + + if (canPlay && shouldPlay) { + if (appState.audioEditorPlaybackTime >= track.audioBuffer.duration) return; + + const source = audioContext.createBufferSource(); + source.buffer = track.audioBuffer; + source.connect(track.gainNode); + source.start(startTime, appState.audioEditorPlaybackTime); + appState.activeAudioSources.push(source); + } + }); + + if (appState.activeAudioSources.length > 0) { + if (appState.audioEditorAnimationId) { + cancelAnimationFrame(appState.audioEditorAnimationId); + } + animationLoop(); + } else { + appState.isAudioEditorPlaying = false; + updateAudioEditorUI(); + } +} + +export function stopAudioEditorPlayback() { + if (!appState.isAudioEditorPlaying) return; + const elapsedTime = (audioContext.currentTime - appState.audioEditorStartTime); + appState.audioEditorPlaybackTime += elapsedTime; + if (appState.audioEditorAnimationId) { + cancelAnimationFrame(appState.audioEditorAnimationId); + appState.audioEditorAnimationId = null; + } + appState.activeAudioSources.forEach(source => { + try { + source.stop(0); + } catch (e) { /* Ignora erros */ } + }); + appState.activeAudioSources = []; + appState.isAudioEditorPlaying = false; + updateAudioEditorUI(); +} + +export function seekAudioEditor(newTime) { + const wasPlaying = appState.isAudioEditorPlaying; + if (wasPlaying) { + stopAudioEditorPlayback(); + } + appState.audioEditorPlaybackTime = newTime; + const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120; + const stepsPerSecond = (bpm / 60) * 4; + const pixelsPerSecond = stepsPerSecond * PIXELS_PER_STEP; + const newPositionPx = newTime * pixelsPerSecond; + updatePlayheadVisual(newPositionPx); + if (wasPlaying) { + startAudioEditorPlayback(); + } } \ No newline at end of file diff --git a/assets/js/creations/config.js b/assets/js/creations/config.js index 4b0ef63..69b9edd 100644 --- a/assets/js/creations/config.js +++ b/assets/js/creations/config.js @@ -7,3 +7,8 @@ export const NOTE_LENGTH = 12; // Constantes para os valores padrão dos knobs export const DEFAULT_VOLUME = 0.8; export const DEFAULT_PAN = 0.0; + +// --- ADICIONADO --- +// Constantes para o layout do editor de áudio +export const PIXELS_PER_STEP = 32; // Cada step (1/16) terá 32px de largura +export const PIXELS_PER_BAR = 512; // 16 steps * 32px/step = 512px por compasso (bar) \ No newline at end of file diff --git a/assets/js/creations/main.js b/assets/js/creations/main.js index 2debcf2..fb7b049 100644 --- a/assets/js/creations/main.js +++ b/assets/js/creations/main.js @@ -11,6 +11,8 @@ import { initializeAudioContext, updateMasterVolume, updateMasterPan, + startAudioEditorPlayback, + stopAudioEditorPlayback, } from "./audio.js"; import { handleFileLoad, generateMmpFile } from "./file.js"; import { @@ -33,6 +35,8 @@ document.addEventListener("DOMContentLoaded", () => { const removeInstrumentBtn = document.getElementById("remove-instrument-btn"); const playBtn = document.getElementById("play-btn"); const stopBtn = document.getElementById("stop-btn"); + const audioEditorPlayBtn = document.getElementById("audio-editor-play-btn"); + const audioEditorStopBtn = document.getElementById("audio-editor-stop-btn"); const rewindBtn = document.getElementById("rewind-btn"); const metronomeBtn = document.getElementById("metronome-btn"); const mmpFileInput = document.getElementById("mmp-file-input"); @@ -248,6 +252,17 @@ document.addEventListener("DOMContentLoaded", () => { }); }); + // Listeners para os controles do editor de áudio + audioEditorPlayBtn.addEventListener("click", () => { + if (appState.isAudioEditorPlaying) { + stopAudioEditorPlayback(); + } else { + startAudioEditorPlayback(); + } + }); + + audioEditorStopBtn.addEventListener("click", stopAudioEditorPlayback); + loadAndRenderSampleBrowser(); renderApp(); setupMasterKnobs(); diff --git a/assets/js/creations/state.js b/assets/js/creations/state.js index b2b9060..1418dcc 100644 --- a/assets/js/creations/state.js +++ b/assets/js/creations/state.js @@ -5,18 +5,25 @@ import { getAudioContext, getMainGainNode, } from "./audio.js"; -import { renderApp } from "./ui.js"; +import { renderApp, renderAudioEditor } from "./ui.js"; import { getTotalSteps } from "./utils.js"; export let appState = { tracks: [], + audioTracks: [], activeTrackId: null, - activePatternIndex: 0, // <-- VOLTOU A SER GLOBAL + activePatternIndex: 0, isPlaying: false, + isAudioEditorPlaying: false, + activeAudioSources: [], + audioEditorStartTime: 0, + audioEditorAnimationId: null, + audioEditorPlaybackTime: 0, playbackIntervalId: null, currentStep: 0, metronomeEnabled: false, originalXmlDoc: null, + currentBeatBasslineName: 'Novo Projeto', masterVolume: DEFAULT_VOLUME, masterPan: DEFAULT_PAN, }; @@ -37,6 +44,54 @@ export async function loadAudioForTrack(track) { return track; } +export function addAudioTrack(samplePath) { + initializeAudioContext(); + const audioContext = getAudioContext(); + const mainGainNode = getMainGainNode(); + + const newAudioTrack = { + id: Date.now() + Math.random(), + name: samplePath.split('/').pop(), + samplePath: samplePath, + audioBuffer: null, + volume: DEFAULT_VOLUME, + pan: DEFAULT_PAN, + isMuted: false, + isSoloed: false, // <-- ADICIONADO: Começa como não-solada + gainNode: audioContext.createGain(), + pannerNode: audioContext.createStereoPanner(), + }; + + newAudioTrack.gainNode.connect(newAudioTrack.pannerNode); + newAudioTrack.pannerNode.connect(mainGainNode); + newAudioTrack.gainNode.gain.value = newAudioTrack.volume; + newAudioTrack.pannerNode.pan.value = newAudioTrack.pan; + + appState.audioTracks.push(newAudioTrack); + + loadAudioForTrack(newAudioTrack).then(() => { + renderAudioEditor(); + }); +} + +// A função de mute agora será a de solo. +export function toggleAudioTrackSolo(trackId) { + const track = appState.audioTracks.find(t => t.id == trackId); + if (track) { + track.isSoloed = !track.isSoloed; + renderAudioEditor(); // Re-renderiza para mostrar a nova cor + } +} + +// Mantemos a função de mute caso precise no futuro, mas ela não está conectada ao botão. +export function toggleAudioTrackMute(trackId) { + const track = appState.audioTracks.find(t => t.id == trackId); + if (track) { + track.isMuted = !track.isMuted; + renderAudioEditor(); + } +} + export function addTrackToState() { initializeAudioContext(); const audioContext = getAudioContext(); @@ -52,7 +107,7 @@ export function addTrackToState() { patterns: referenceTrack ? referenceTrack.patterns.map(p => ({ name: p.name, steps: new Array(p.steps.length).fill(false), pos: p.pos })) : [{ name: "Pattern 1", steps: new Array(totalSteps).fill(false), pos: 0 }], - // activePatternIndex foi removido daqui + activePatternIndex: 0, volume: DEFAULT_VOLUME, pan: DEFAULT_PAN, gainNode: audioContext.createGain(), @@ -64,18 +119,12 @@ export function addTrackToState() { newTrack.pannerNode.pan.value = newTrack.pan; appState.tracks.push(newTrack); - if (!appState.activeTrackId) { - appState.activeTrackId = newTrack.id; - } renderApp(); } export function removeLastTrackFromState() { if (appState.tracks.length > 0) { - const removedTrack = appState.tracks.pop(); - if (appState.activeTrackId === removedTrack.id) { - appState.activeTrackId = appState.tracks[0]?.id || null; - } + appState.tracks.pop(); renderApp(); } } @@ -87,18 +136,14 @@ export async function updateTrackSample(trackId, samplePath) { track.name = samplePath.split("/").pop(); track.audioBuffer = null; await loadAudioForTrack(track); - const trackLane = document.querySelector(`.track-lane[data-track-id="${trackId}"] .track-name`); - if (trackLane) { - trackLane.textContent = track.name; - } + renderApp(); } } export function toggleStepState(trackId, stepIndex) { const track = appState.tracks.find((t) => t.id == trackId); if (track && track.patterns && track.patterns.length > 0) { - // Usa o índice GLOBAL para saber qual pattern modificar - const activePattern = track.patterns[appState.activePatternIndex]; + const activePattern = track.patterns[track.activePatternIndex]; if (activePattern && activePattern.steps.length > stepIndex) { activePattern.steps[stepIndex] = !activePattern.steps[stepIndex]; } @@ -106,7 +151,7 @@ export function toggleStepState(trackId, stepIndex) { } export function updateTrackVolume(trackId, volume) { - const track = appState.tracks.find((t) => t.id == trackId); + const track = appState.tracks.find((t) => t.id == trackId) || appState.audioTracks.find((t) => t.id == trackId); if (track) { const clampedVolume = Math.max(0, Math.min(1.5, volume)); track.volume = clampedVolume; @@ -117,7 +162,7 @@ export function updateTrackVolume(trackId, volume) { } export function updateTrackPan(trackId, pan) { - const track = appState.tracks.find((t) => t.id == trackId); + const track = appState.tracks.find((t) => t.id == trackId) || appState.audioTracks.find((t) => t.id == trackId); if (track) { const clampedPan = Math.max(-1, Math.min(1, pan)); track.pan = clampedPan; diff --git a/assets/js/creations/ui.js b/assets/js/creations/ui.js index a553b17..d684a60 100644 --- a/assets/js/creations/ui.js +++ b/assets/js/creations/ui.js @@ -5,17 +5,34 @@ import { updateTrackSample, updateTrackVolume, updateTrackPan, + addAudioTrack, + toggleAudioTrackSolo, } from "./state.js"; -import { playSample, stopPlayback } from "./audio.js"; +import { playSample, stopPlayback, seekAudioEditor } from "./audio.js"; import { getTotalSteps } from "./utils.js"; import { loadProjectFromServer } from "./file.js"; +import { drawWaveform } from "./waveform.js"; +import { PIXELS_PER_STEP, PIXELS_PER_BAR } from "./config.js"; + +export function updateAudioEditorUI() { + const playBtn = document.getElementById('audio-editor-play-btn'); + if (playBtn) { + if (appState.isAudioEditorPlaying) { + playBtn.classList.remove('fa-play'); + playBtn.classList.add('fa-pause'); + } else { + playBtn.classList.remove('fa-pause'); + playBtn.classList.add('fa-play'); + } + } +} let samplePathMap = {}; const globalPatternSelector = document.getElementById('global-pattern-selector'); if (globalPatternSelector) { globalPatternSelector.addEventListener('change', () => { - // A linha stopPlayback() foi REMOVIDA daqui, permitindo a troca em tempo real. + stopPlayback(); appState.activePatternIndex = parseInt(globalPatternSelector.value, 10); const firstTrack = appState.tracks[0]; @@ -105,6 +122,137 @@ function buildSamplePathMap(tree, currentPath) { } } +export function renderAudioEditor() { + const audioEditor = document.querySelector('.audio-editor'); + const audioTrackContainer = document.getElementById('audio-track-container'); + if (!audioEditor || !audioTrackContainer) return; + + audioEditor.ondragover = (e) => { + e.preventDefault(); + audioEditor.classList.add("drag-over"); + }; + audioEditor.ondragleave = () => { + audioEditor.classList.remove("drag-over"); + }; + audioEditor.ondrop = (e) => { + e.preventDefault(); + audioEditor.classList.remove("drag-over"); + const filePath = e.dataTransfer.getData("text/plain"); + if (filePath) { + addAudioTrack(filePath); + } + }; + + audioTrackContainer.innerHTML = ''; + + appState.audioTracks.forEach(trackData => { + const audioTrackLane = document.createElement('div'); + audioTrackLane.className = 'audio-track-lane'; + audioTrackLane.dataset.trackId = trackData.id; + + audioTrackLane.innerHTML = ` +