From c4980aa01b614e4af6fdbc3f2db624f2278a78e4 Mon Sep 17 00:00:00 2001 From: JotaChina Date: Sat, 27 Dec 2025 12:48:26 -0300 Subject: [PATCH] editando e enviando patterns na playlist --- assets/css/creator.css | 36 +++ assets/js/creations/audio/audio_ui.js | 393 ++++++++++++++++++++++---- 2 files changed, 369 insertions(+), 60 deletions(-) diff --git a/assets/css/creator.css b/assets/css/creator.css index eaa83085..e7fbf043 100755 --- a/assets/css/creator.css +++ b/assets/css/creator.css @@ -242,6 +242,42 @@ body.sidebar-hidden .sample-browser { width: 0; min-width: 0; border-right: none #create-room-btn { width: auto; padding-left: 12px; padding-right: 16px; color: var(--text-light); } #create-room-btn:hover { background-color: var(--accent-color); color: var(--text-dark); } +/* ========================================================= + Playlist / Bassline clips (patterns) + ========================================================= */ + +.timeline-clip.bassline-clip { + cursor: grab; +} + +.timeline-clip.bassline-clip.dragging { + cursor: grabbing; +} + +.timeline-clip.bassline-clip.selected { + outline: 2px solid rgba(255, 255, 255, 0.65); + outline-offset: -2px; +} + +/* Handles de resize (L/R) do clip de pattern */ +.timeline-clip.bassline-clip .pattern-resize-handle { + position: absolute; + top: 0; + bottom: 0; + width: 8px; + z-index: 8; /* acima do overlay e label */ + cursor: ew-resize; + background: rgba(255, 255, 255, 0.10); +} + +.timeline-clip.bassline-clip .pattern-resize-handle.left { + left: 0; +} + +.timeline-clip.bassline-clip .pattern-resize-handle.right { + right: 0; +} + /* =============================================== */ /* BEAT EDITOR / STEP SEQUENCER /* =============================================== */ diff --git a/assets/js/creations/audio/audio_ui.js b/assets/js/creations/audio/audio_ui.js index e2c8864e..6f10a904 100755 --- a/assets/js/creations/audio/audio_ui.js +++ b/assets/js/creations/audio/audio_ui.js @@ -804,22 +804,47 @@ export function renderAudioEditor() { newTrackContainer.addEventListener("mousedown", (e) => { document.getElementById("timeline-context-menu").style.display = "none"; document.getElementById("ruler-context-menu").style.display = "none"; + const clipElement = e.target.closest(".timeline-clip"); - + const isBasslineClip = + !!(clipElement && clipElement.classList.contains("bassline-clip")); + + // ✅ limpa seleções ao clicar no vazio (sem mexer no RMB) if (!clipElement && e.button !== 2) { if (appState.global.selectedClipId) { appState.global.selectedClipId = null; - newTrackContainer - .querySelectorAll(".timeline-clip.selected") - .forEach((c) => c.classList.remove("selected")); } + if (appState.global.selectedPlaylistClipId) { + appState.global.selectedPlaylistClipId = null; + appState.global.selectedPlaylistPatternIndex = null; + } + + newTrackContainer + .querySelectorAll(".timeline-clip.selected") + .forEach((c) => c.classList.remove("selected")); } const currentPixelsPerSecond = getPixelsPerSecond(); const handle = e.target.closest(".clip-resize-handle"); + const patternHandle = e.target.closest(".pattern-resize-handle"); - // Slice Tool - if (appState.global.sliceToolActive && clipElement && !clipElement.classList.contains("bassline-clip")) { + // ✅ se clicou num clip de áudio, deseleciona a pattern da playlist + if (clipElement && !isBasslineClip && e.button === 0) { + if (appState.global.selectedPlaylistClipId) { + appState.global.selectedPlaylistClipId = null; + appState.global.selectedPlaylistPatternIndex = null; + newTrackContainer + .querySelectorAll(".bassline-clip.selected") + .forEach((c) => c.classList.remove("selected")); + } + } + + // Slice Tool (áudio apenas) + if ( + appState.global.sliceToolActive && + clipElement && + !clipElement.classList.contains("bassline-clip") + ) { e.preventDefault(); e.stopPropagation(); const clipId = clipElement.dataset.clipId; @@ -843,14 +868,193 @@ export function renderAudioEditor() { return; } - // Resize Handle + // ========================================================= + // ✅ BASSLINE / PATTERN CLIPS (drag horizontal + resize L/R) + // ========================================================= + if (isBasslineClip && e.button === 0) { + e.preventDefault(); + + _ensureGlobalPlaylistSelectionFields(); + + const patternIndex = Number(clipElement.dataset.patternIndex ?? 0); + const plClipId = String(clipElement.dataset.plClipId || ""); + if (!plClipId) return; + + // seleção visual/estado + appState.global.selectedPlaylistClipId = plClipId; + appState.global.selectedPlaylistPatternIndex = patternIndex; + + // desmarca seleção de áudio se tiver + if (appState.global.selectedClipId) { + appState.global.selectedClipId = null; + newTrackContainer + .querySelectorAll(".timeline-clip.selected") + .forEach((c) => c.classList.remove("selected")); + } + + newTrackContainer + .querySelectorAll(".bassline-clip.selected") + .forEach((c) => c.classList.remove("selected")); + clipElement.classList.add("selected"); + + // (opcional) sync de seleção + sendActionSafe({ + type: "SELECT_PLAYLIST_PATTERN_CLIP", + patternIndex, + clipId: plClipId, + }); + + const model = _getPlaylistClipModel(patternIndex, plClipId); + if (!model) return; + + const initialMouseX = e.clientX; + const initialScrollLeft = scrollEl.scrollLeft; + + const initialPosTicks = Number(model.pos || 0); + const initialLenTicks = Math.max( + PL_MIN_LEN_TICKS, + Number(model.len || PL_MIN_LEN_TICKS) + ); + const initialEndTicks = initialPosTicks + initialLenTicks; + + const previewUpdate = (posTicks, lenTicks) => { + const leftPx = _ticksToPx(posTicks, stepWidthPx); + const widthPx = Math.max(1, _ticksToPx(lenTicks, stepWidthPx)); + clipElement.style.left = `${leftPx}px`; + clipElement.style.width = `${widthPx}px`; + // mantém grid alinhado com a timeline + clipElement.style.backgroundPosition = `-${leftPx}px 0px`; + }; + + // ---------- RESIZE ---------- + if (patternHandle) { + const handleType = patternHandle.classList.contains("left") + ? "left" + : "right"; + + clipElement.classList.add("dragging"); + + const onMouseMove = (moveEvent) => { + const deltaPx = + (moveEvent.clientX - initialMouseX) + + (scrollEl.scrollLeft - initialScrollLeft); + + const deltaTicks = _pxToTicks(deltaPx, stepWidthPx); + + let newPos = initialPosTicks; + let newLen = initialLenTicks; + + if (handleType === "right") { + let newEnd = _snapTicks(initialEndTicks + deltaTicks, PL_SNAP_TICKS); + newEnd = Math.max(initialPosTicks + PL_MIN_LEN_TICKS, newEnd); + newLen = newEnd - initialPosTicks; + } else { + newPos = _snapTicks(initialPosTicks + deltaTicks, PL_SNAP_TICKS); + newPos = Math.max(0, newPos); + newPos = Math.min(newPos, initialEndTicks - PL_MIN_LEN_TICKS); + newLen = initialEndTicks - newPos; + } + + previewUpdate(newPos, newLen); + }; + + const onMouseUp = () => { + clipElement.classList.remove("dragging"); + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + + // converte estado final (px -> ticks) com snap + const finalLeftPx = clipElement.offsetLeft; + const finalWidthPx = clipElement.offsetWidth; + + let finalPos = _pxToTicks(finalLeftPx, stepWidthPx); + let finalLen = _pxToTicks(finalWidthPx, stepWidthPx); + + finalPos = _snapTicks(finalPos, PL_SNAP_TICKS); + finalLen = _snapTicks(finalLen, PL_SNAP_TICKS); + + finalPos = Math.max(0, finalPos); + finalLen = Math.max(PL_MIN_LEN_TICKS, finalLen); + + _updatePlaylistClipLocal(patternIndex, plClipId, { + pos: finalPos, + len: finalLen, + }); + + sendActionSafe({ + type: "UPDATE_PLAYLIST_PATTERN_CLIP", + patternIndex, + clipId: plClipId, + pos: finalPos, + len: finalLen, + }); + + renderAudioEditor(); + restartAudioEditorIfPlaying(); + }; + + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + return; + } + + // ---------- DRAG (horizontal apenas) ---------- + clipElement.classList.add("dragging"); + + const onMouseMove = (moveEvent) => { + const deltaPx = + (moveEvent.clientX - initialMouseX) + + (scrollEl.scrollLeft - initialScrollLeft); + + let newPos = initialPosTicks + _pxToTicks(deltaPx, stepWidthPx); + newPos = _snapTicks(newPos, PL_SNAP_TICKS); + newPos = Math.max(0, newPos); + + previewUpdate(newPos, initialLenTicks); + }; + + const onMouseUp = () => { + clipElement.classList.remove("dragging"); + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + + const finalLeftPx = clipElement.offsetLeft; + let finalPos = _pxToTicks(finalLeftPx, stepWidthPx); + finalPos = _snapTicks(finalPos, PL_SNAP_TICKS); + finalPos = Math.max(0, finalPos); + + _updatePlaylistClipLocal(patternIndex, plClipId, { + pos: finalPos, + len: initialLenTicks, + }); + + sendActionSafe({ + type: "UPDATE_PLAYLIST_PATTERN_CLIP", + patternIndex, + clipId: plClipId, + pos: finalPos, + len: initialLenTicks, + }); + + renderAudioEditor(); + restartAudioEditorIfPlaying(); + }; + + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + return; + } + + // ========================================================= + // Resize Handle (ÁUDIO) + // ========================================================= if (handle) { e.preventDefault(); e.stopPropagation(); const clipId = clipElement.dataset.clipId; const clip = appState.audio.clips.find((c) => c.id == clipId); if (!clip || !clip.buffer) return; - + const handleType = handle.classList.contains("left") ? "left" : "right"; const initialMouseX = e.clientX; const secondsPerStep = getSecondsPerStep(); @@ -859,9 +1063,10 @@ export function renderAudioEditor() { const initialStartTime = clip.startTimeInSeconds; const initialDuration = clip.durationInSeconds; const initialOffset = clip.offset || 0; - const initialOriginalDuration = clip.originalDuration || clip.buffer.duration; + const initialOriginalDuration = + clip.originalDuration || clip.buffer.duration; const bufferStartTime = initialStartTime - initialOffset; - + const onMouseMove = (moveEvent) => { const deltaX = moveEvent.clientX - initialMouseX; if (appState.global.resizeMode === "trim") { @@ -872,71 +1077,125 @@ export function renderAudioEditor() { newEndTime = Math.max(initialStartTime + secondsPerStep, newEndTime); const maxEndTime = bufferStartTime + initialOriginalDuration; newEndTime = Math.min(newEndTime, maxEndTime); - clipElement.style.width = `${(newEndTime - initialStartTime) * currentPixelsPerSecond}px`; + clipElement.style.width = `${ + (newEndTime - initialStartTime) * currentPixelsPerSecond + }px`; } else if (handleType === "left") { let newLeftPx = initialLeftPx + deltaX; let newStartTime = newLeftPx / currentPixelsPerSecond; newStartTime = quantizeTime(newStartTime); - const minStartTime = initialStartTime + initialDuration - secondsPerStep; + const minStartTime = + initialStartTime + initialDuration - secondsPerStep; newStartTime = Math.min(newStartTime, minStartTime); newStartTime = Math.max(bufferStartTime, newStartTime); const newLeftFinalPx = newStartTime * currentPixelsPerSecond; - const newWidthFinalPx = (initialStartTime + initialDuration - newStartTime) * currentPixelsPerSecond; + const newWidthFinalPx = + (initialStartTime + initialDuration - newStartTime) * + currentPixelsPerSecond; clipElement.style.left = `${newLeftFinalPx}px`; clipElement.style.width = `${newWidthFinalPx}px`; } } else if (appState.global.resizeMode === "stretch") { - if (handleType === "right") { - let newWidthPx = initialWidthPx + deltaX; - let newDuration = newWidthPx / currentPixelsPerSecond; - let newEndTime = quantizeTime(initialStartTime + newDuration); - newEndTime = Math.max(initialStartTime + secondsPerStep, newEndTime); - clipElement.style.width = `${(newEndTime - initialStartTime) * currentPixelsPerSecond}px`; - } else if (handleType === "left") { - let newLeftPx = initialLeftPx + deltaX; - let newStartTime = newLeftPx / currentPixelsPerSecond; - newStartTime = quantizeTime(newStartTime); - const minStartTime = initialStartTime + initialDuration - secondsPerStep; - newStartTime = Math.min(newStartTime, minStartTime); - const newLeftFinalPx = newStartTime * currentPixelsPerSecond; - const newWidthFinalPx = (initialStartTime + initialDuration - newStartTime) * currentPixelsPerSecond; - clipElement.style.left = `${newLeftFinalPx}px`; - clipElement.style.width = `${newWidthFinalPx}px`; - } + if (handleType === "right") { + let newWidthPx = initialWidthPx + deltaX; + let newDuration = newWidthPx / currentPixelsPerSecond; + let newEndTime = quantizeTime(initialStartTime + newDuration); + newEndTime = Math.max(initialStartTime + secondsPerStep, newEndTime); + clipElement.style.width = `${ + (newEndTime - initialStartTime) * currentPixelsPerSecond + }px`; + } else if (handleType === "left") { + let newLeftPx = initialLeftPx + deltaX; + let newStartTime = newLeftPx / currentPixelsPerSecond; + newStartTime = quantizeTime(newStartTime); + const minStartTime = + initialStartTime + initialDuration - secondsPerStep; + newStartTime = Math.min(newStartTime, minStartTime); + const newLeftFinalPx = newStartTime * currentPixelsPerSecond; + const newWidthFinalPx = + (initialStartTime + initialDuration - newStartTime) * + currentPixelsPerSecond; + clipElement.style.left = `${newLeftFinalPx}px`; + clipElement.style.width = `${newWidthFinalPx}px`; + } } }; - const onMouseUp = (upEvent) => { + const onMouseUp = () => { document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); const finalLeftPx = clipElement.offsetLeft; const finalWidthPx = clipElement.offsetWidth; const newStartTime = finalLeftPx / currentPixelsPerSecond; const newDuration = finalWidthPx / currentPixelsPerSecond; - + if (appState.global.resizeMode === "trim") { - const newOffset = newStartTime - bufferStartTime; - if(handleType === "right") { - updateAudioClipProperties(clipId, { durationInSeconds: newDuration, pitch: 0 }); - sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { durationInSeconds: newDuration, pitch: 0 } }); - } else { - updateAudioClipProperties(clipId, { startTimeInSeconds: newStartTime, durationInSeconds: newDuration, offset: newOffset, pitch: 0 }); - sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { startTimeInSeconds: newStartTime, durationInSeconds: newDuration, offset: newOffset, pitch: 0 } }); - } + const newOffset = newStartTime - bufferStartTime; + if (handleType === "right") { + updateAudioClipProperties(clipId, { + durationInSeconds: newDuration, + pitch: 0, + }); + sendActionSafe({ + type: "UPDATE_AUDIO_CLIP", + clipId, + props: { durationInSeconds: newDuration, pitch: 0 }, + }); + } else { + updateAudioClipProperties(clipId, { + startTimeInSeconds: newStartTime, + durationInSeconds: newDuration, + offset: newOffset, + pitch: 0, + }); + sendActionSafe({ + type: "UPDATE_AUDIO_CLIP", + clipId, + props: { + startTimeInSeconds: newStartTime, + durationInSeconds: newDuration, + offset: newOffset, + pitch: 0, + }, + }); + } } else { - const newPlaybackRate = initialOriginalDuration / newDuration; - const newPitch = 12 * Math.log2(newPlaybackRate); - if(handleType === "right") { - updateAudioClipProperties(clipId, { durationInSeconds: newDuration, pitch: newPitch, offset: 0 }); - sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { durationInSeconds: newDuration, pitch: newPitch, offset: 0 } }); - } else { - updateAudioClipProperties(clipId, { startTimeInSeconds: newStartTime, durationInSeconds: newDuration, pitch: newPitch, offset: 0 }); - sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { startTimeInSeconds: newStartTime, durationInSeconds: newDuration, pitch: newPitch, offset: 0 } }); - } + const newPlaybackRate = initialOriginalDuration / newDuration; + const newPitch = 12 * Math.log2(newPlaybackRate); + if (handleType === "right") { + updateAudioClipProperties(clipId, { + durationInSeconds: newDuration, + pitch: newPitch, + offset: 0, + }); + sendActionSafe({ + type: "UPDATE_AUDIO_CLIP", + clipId, + props: { durationInSeconds: newDuration, pitch: newPitch, offset: 0 }, + }); + } else { + updateAudioClipProperties(clipId, { + startTimeInSeconds: newStartTime, + durationInSeconds: newDuration, + pitch: newPitch, + offset: 0, + }); + sendActionSafe({ + type: "UPDATE_AUDIO_CLIP", + clipId, + props: { + startTimeInSeconds: newStartTime, + durationInSeconds: newDuration, + pitch: newPitch, + offset: 0, + }, + }); + } } restartAudioEditorIfPlaying(); renderAudioEditor(); }; + document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); return; @@ -946,8 +1205,9 @@ export function renderAudioEditor() { if (clipElement && !clipElement.classList.contains("bassline-clip")) { const clipId = clipElement.dataset.clipId; - // 🔑 pega o clip no estado pra ter o tempo inicial real - const clipModel = appState.audio.clips.find(c => String(c.id) === String(clipId)); + const clipModel = appState.audio.clips.find( + (c) => String(c.id) === String(clipId) + ); const initialStartTime = Number(clipModel?.startTimeInSeconds || 0); e.preventDefault(); @@ -961,8 +1221,13 @@ export function renderAudioEditor() { const deltaX = moveEvent.clientX - initialMouseX; clipElement.style.transform = `translateX(${deltaX}px)`; - const overElement = document.elementFromPoint(moveEvent.clientX, moveEvent.clientY); - const overLane = overElement ? overElement.closest(".audio-track-lane") : null; + const overElement = document.elementFromPoint( + moveEvent.clientX, + moveEvent.clientY + ); + const overLane = overElement + ? overElement.closest(".audio-track-lane") + : null; if (overLane && overLane !== lastOverLane) { if (lastOverLane) lastOverLane.classList.remove("drag-over"); overLane.classList.add("drag-over"); @@ -982,15 +1247,24 @@ export function renderAudioEditor() { const newTrackId = finalLane.dataset.trackId; - // ✅ delta do mouse + delta de scroll durante o drag (se houver) - const deltaX = (upEvent.clientX - initialMouseX) + (scrollEl.scrollLeft - initialScrollLeft); + const deltaX = + (upEvent.clientX - initialMouseX) + + (scrollEl.scrollLeft - initialScrollLeft); - let newStartTime = initialStartTime + (deltaX / currentPixelsPerSecond); + let newStartTime = + initialStartTime + deltaX / currentPixelsPerSecond; newStartTime = Math.max(0, newStartTime); newStartTime = quantizeTime(newStartTime); - updateAudioClipProperties(clipId, { trackId: newTrackId, startTimeInSeconds: newStartTime }); - sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { trackId: newTrackId, startTimeInSeconds: newStartTime } }); + updateAudioClipProperties(clipId, { + trackId: newTrackId, + startTimeInSeconds: newStartTime, + }); + sendActionSafe({ + type: "UPDATE_AUDIO_CLIP", + clipId, + props: { trackId: newTrackId, startTimeInSeconds: newStartTime }, + }); renderAudioEditor(); }; @@ -999,7 +1273,6 @@ export function renderAudioEditor() { return; } - // Seek na Pista const timelineContainer = e.target.closest(".timeline-container"); if (timelineContainer) { @@ -1012,7 +1285,7 @@ export function renderAudioEditor() { const newTime = absoluteX / currentPixelsPerSecond; sendAction({ type: "SET_SEEK_TIME", seekTime: newTime }); }; - handleSeek(e); + handleSeek(e); const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent); const onMouseUpSeek = () => { document.removeEventListener("mousemove", onMouseMoveSeek);