editando e enviando patterns na playlist
Deploy / Deploy (push) Successful in 2m0s Details

This commit is contained in:
JotaChina 2025-12-27 12:48:26 -03:00
parent 8ea53596be
commit c4980aa01b
2 changed files with 369 additions and 60 deletions

View File

@ -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 { 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); } #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 /* BEAT EDITOR / STEP SEQUENCER
/* =============================================== */ /* =============================================== */

View File

@ -804,22 +804,47 @@ export function renderAudioEditor() {
newTrackContainer.addEventListener("mousedown", (e) => { newTrackContainer.addEventListener("mousedown", (e) => {
document.getElementById("timeline-context-menu").style.display = "none"; document.getElementById("timeline-context-menu").style.display = "none";
document.getElementById("ruler-context-menu").style.display = "none"; document.getElementById("ruler-context-menu").style.display = "none";
const clipElement = e.target.closest(".timeline-clip");
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 (!clipElement && e.button !== 2) {
if (appState.global.selectedClipId) { if (appState.global.selectedClipId) {
appState.global.selectedClipId = null; 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 currentPixelsPerSecond = getPixelsPerSecond();
const handle = e.target.closest(".clip-resize-handle"); const handle = e.target.closest(".clip-resize-handle");
const patternHandle = e.target.closest(".pattern-resize-handle");
// Slice Tool // ✅ se clicou num clip de áudio, deseleciona a pattern da playlist
if (appState.global.sliceToolActive && clipElement && !clipElement.classList.contains("bassline-clip")) { 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.preventDefault();
e.stopPropagation(); e.stopPropagation();
const clipId = clipElement.dataset.clipId; const clipId = clipElement.dataset.clipId;
@ -843,7 +868,186 @@ export function renderAudioEditor() {
return; 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) { if (handle) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -859,7 +1063,8 @@ export function renderAudioEditor() {
const initialStartTime = clip.startTimeInSeconds; const initialStartTime = clip.startTimeInSeconds;
const initialDuration = clip.durationInSeconds; const initialDuration = clip.durationInSeconds;
const initialOffset = clip.offset || 0; const initialOffset = clip.offset || 0;
const initialOriginalDuration = clip.originalDuration || clip.buffer.duration; const initialOriginalDuration =
clip.originalDuration || clip.buffer.duration;
const bufferStartTime = initialStartTime - initialOffset; const bufferStartTime = initialStartTime - initialOffset;
const onMouseMove = (moveEvent) => { const onMouseMove = (moveEvent) => {
@ -872,41 +1077,51 @@ export function renderAudioEditor() {
newEndTime = Math.max(initialStartTime + secondsPerStep, newEndTime); newEndTime = Math.max(initialStartTime + secondsPerStep, newEndTime);
const maxEndTime = bufferStartTime + initialOriginalDuration; const maxEndTime = bufferStartTime + initialOriginalDuration;
newEndTime = Math.min(newEndTime, maxEndTime); newEndTime = Math.min(newEndTime, maxEndTime);
clipElement.style.width = `${(newEndTime - initialStartTime) * currentPixelsPerSecond}px`; clipElement.style.width = `${
(newEndTime - initialStartTime) * currentPixelsPerSecond
}px`;
} else if (handleType === "left") { } else if (handleType === "left") {
let newLeftPx = initialLeftPx + deltaX; let newLeftPx = initialLeftPx + deltaX;
let newStartTime = newLeftPx / currentPixelsPerSecond; let newStartTime = newLeftPx / currentPixelsPerSecond;
newStartTime = quantizeTime(newStartTime); newStartTime = quantizeTime(newStartTime);
const minStartTime = initialStartTime + initialDuration - secondsPerStep; const minStartTime =
initialStartTime + initialDuration - secondsPerStep;
newStartTime = Math.min(newStartTime, minStartTime); newStartTime = Math.min(newStartTime, minStartTime);
newStartTime = Math.max(bufferStartTime, newStartTime); newStartTime = Math.max(bufferStartTime, newStartTime);
const newLeftFinalPx = newStartTime * currentPixelsPerSecond; const newLeftFinalPx = newStartTime * currentPixelsPerSecond;
const newWidthFinalPx = (initialStartTime + initialDuration - newStartTime) * currentPixelsPerSecond; const newWidthFinalPx =
(initialStartTime + initialDuration - newStartTime) *
currentPixelsPerSecond;
clipElement.style.left = `${newLeftFinalPx}px`; clipElement.style.left = `${newLeftFinalPx}px`;
clipElement.style.width = `${newWidthFinalPx}px`; clipElement.style.width = `${newWidthFinalPx}px`;
} }
} else if (appState.global.resizeMode === "stretch") { } else if (appState.global.resizeMode === "stretch") {
if (handleType === "right") { if (handleType === "right") {
let newWidthPx = initialWidthPx + deltaX; let newWidthPx = initialWidthPx + deltaX;
let newDuration = newWidthPx / currentPixelsPerSecond; let newDuration = newWidthPx / currentPixelsPerSecond;
let newEndTime = quantizeTime(initialStartTime + newDuration); let newEndTime = quantizeTime(initialStartTime + newDuration);
newEndTime = Math.max(initialStartTime + secondsPerStep, newEndTime); newEndTime = Math.max(initialStartTime + secondsPerStep, newEndTime);
clipElement.style.width = `${(newEndTime - initialStartTime) * currentPixelsPerSecond}px`; clipElement.style.width = `${
} else if (handleType === "left") { (newEndTime - initialStartTime) * currentPixelsPerSecond
let newLeftPx = initialLeftPx + deltaX; }px`;
let newStartTime = newLeftPx / currentPixelsPerSecond; } else if (handleType === "left") {
newStartTime = quantizeTime(newStartTime); let newLeftPx = initialLeftPx + deltaX;
const minStartTime = initialStartTime + initialDuration - secondsPerStep; let newStartTime = newLeftPx / currentPixelsPerSecond;
newStartTime = Math.min(newStartTime, minStartTime); newStartTime = quantizeTime(newStartTime);
const newLeftFinalPx = newStartTime * currentPixelsPerSecond; const minStartTime =
const newWidthFinalPx = (initialStartTime + initialDuration - newStartTime) * currentPixelsPerSecond; initialStartTime + initialDuration - secondsPerStep;
clipElement.style.left = `${newLeftFinalPx}px`; newStartTime = Math.min(newStartTime, minStartTime);
clipElement.style.width = `${newWidthFinalPx}px`; 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("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp); document.removeEventListener("mouseup", onMouseUp);
const finalLeftPx = clipElement.offsetLeft; const finalLeftPx = clipElement.offsetLeft;
@ -915,28 +1130,72 @@ export function renderAudioEditor() {
const newDuration = finalWidthPx / currentPixelsPerSecond; const newDuration = finalWidthPx / currentPixelsPerSecond;
if (appState.global.resizeMode === "trim") { if (appState.global.resizeMode === "trim") {
const newOffset = newStartTime - bufferStartTime; const newOffset = newStartTime - bufferStartTime;
if(handleType === "right") { if (handleType === "right") {
updateAudioClipProperties(clipId, { durationInSeconds: newDuration, pitch: 0 }); updateAudioClipProperties(clipId, {
sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { durationInSeconds: newDuration, pitch: 0 } }); durationInSeconds: newDuration,
} else { pitch: 0,
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 } }); 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 { } else {
const newPlaybackRate = initialOriginalDuration / newDuration; const newPlaybackRate = initialOriginalDuration / newDuration;
const newPitch = 12 * Math.log2(newPlaybackRate); const newPitch = 12 * Math.log2(newPlaybackRate);
if(handleType === "right") { if (handleType === "right") {
updateAudioClipProperties(clipId, { durationInSeconds: newDuration, pitch: newPitch, offset: 0 }); updateAudioClipProperties(clipId, {
sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { durationInSeconds: newDuration, pitch: newPitch, offset: 0 } }); durationInSeconds: newDuration,
} else { pitch: newPitch,
updateAudioClipProperties(clipId, { startTimeInSeconds: newStartTime, durationInSeconds: newDuration, pitch: newPitch, offset: 0 }); offset: 0,
sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { startTimeInSeconds: newStartTime, 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(); restartAudioEditorIfPlaying();
renderAudioEditor(); renderAudioEditor();
}; };
document.addEventListener("mousemove", onMouseMove); document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp); document.addEventListener("mouseup", onMouseUp);
return; return;
@ -946,8 +1205,9 @@ export function renderAudioEditor() {
if (clipElement && !clipElement.classList.contains("bassline-clip")) { if (clipElement && !clipElement.classList.contains("bassline-clip")) {
const clipId = clipElement.dataset.clipId; const clipId = clipElement.dataset.clipId;
// 🔑 pega o clip no estado pra ter o tempo inicial real const clipModel = appState.audio.clips.find(
const clipModel = appState.audio.clips.find(c => String(c.id) === String(clipId)); (c) => String(c.id) === String(clipId)
);
const initialStartTime = Number(clipModel?.startTimeInSeconds || 0); const initialStartTime = Number(clipModel?.startTimeInSeconds || 0);
e.preventDefault(); e.preventDefault();
@ -961,8 +1221,13 @@ export function renderAudioEditor() {
const deltaX = moveEvent.clientX - initialMouseX; const deltaX = moveEvent.clientX - initialMouseX;
clipElement.style.transform = `translateX(${deltaX}px)`; clipElement.style.transform = `translateX(${deltaX}px)`;
const overElement = document.elementFromPoint(moveEvent.clientX, moveEvent.clientY); const overElement = document.elementFromPoint(
const overLane = overElement ? overElement.closest(".audio-track-lane") : null; moveEvent.clientX,
moveEvent.clientY
);
const overLane = overElement
? overElement.closest(".audio-track-lane")
: null;
if (overLane && overLane !== lastOverLane) { if (overLane && overLane !== lastOverLane) {
if (lastOverLane) lastOverLane.classList.remove("drag-over"); if (lastOverLane) lastOverLane.classList.remove("drag-over");
overLane.classList.add("drag-over"); overLane.classList.add("drag-over");
@ -982,15 +1247,24 @@ export function renderAudioEditor() {
const newTrackId = finalLane.dataset.trackId; const newTrackId = finalLane.dataset.trackId;
// ✅ delta do mouse + delta de scroll durante o drag (se houver) const deltaX =
const deltaX = (upEvent.clientX - initialMouseX) + (scrollEl.scrollLeft - initialScrollLeft); (upEvent.clientX - initialMouseX) +
(scrollEl.scrollLeft - initialScrollLeft);
let newStartTime = initialStartTime + (deltaX / currentPixelsPerSecond); let newStartTime =
initialStartTime + deltaX / currentPixelsPerSecond;
newStartTime = Math.max(0, newStartTime); newStartTime = Math.max(0, newStartTime);
newStartTime = quantizeTime(newStartTime); newStartTime = quantizeTime(newStartTime);
updateAudioClipProperties(clipId, { trackId: newTrackId, startTimeInSeconds: newStartTime }); updateAudioClipProperties(clipId, {
sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { trackId: newTrackId, startTimeInSeconds: newStartTime } }); trackId: newTrackId,
startTimeInSeconds: newStartTime,
});
sendActionSafe({
type: "UPDATE_AUDIO_CLIP",
clipId,
props: { trackId: newTrackId, startTimeInSeconds: newStartTime },
});
renderAudioEditor(); renderAudioEditor();
}; };
@ -999,7 +1273,6 @@ export function renderAudioEditor() {
return; return;
} }
// Seek na Pista // Seek na Pista
const timelineContainer = e.target.closest(".timeline-container"); const timelineContainer = e.target.closest(".timeline-container");
if (timelineContainer) { if (timelineContainer) {