editando patterns na playlist
Deploy / Deploy (push) Successful in 1m55s Details

This commit is contained in:
JotaChina 2025-12-27 14:04:43 -03:00
parent 6bb8cb8dad
commit f7c81dd1de
1 changed files with 61 additions and 352 deletions

View File

@ -188,10 +188,6 @@ export function renderAudioEditor() {
if (!audioEditor || !existingTrackContainer) return; if (!audioEditor || !existingTrackContainer) return;
// ✅ Salva o scroll atual (senão toda edição “pula” pro início)
const prevScrollLeft = existingTrackContainer.scrollLeft || 0;
const prevScrollTop = existingTrackContainer.scrollTop || 0;
_ensureGlobalPlaylistSelectionFields(); _ensureGlobalPlaylistSelectionFields();
_installPlaylistKeybindOnce(); _installPlaylistKeybindOnce();
@ -808,47 +804,22 @@ 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;
}
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");
// ✅ 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 newTrackContainer
.querySelectorAll(".bassline-clip.selected") .querySelectorAll(".timeline-clip.selected")
.forEach((c) => c.classList.remove("selected")); .forEach((c) => c.classList.remove("selected"));
} }
} }
// Slice Tool (áudio apenas) const currentPixelsPerSecond = getPixelsPerSecond();
if ( const handle = e.target.closest(".clip-resize-handle");
appState.global.sliceToolActive &&
clipElement && // Slice Tool
!clipElement.classList.contains("bassline-clip") 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;
@ -872,193 +843,14 @@ 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();
const clipId = clipElement.dataset.clipId; const clipId = clipElement.dataset.clipId;
const clip = appState.audio.clips.find((c) => c.id == clipId); const clip = appState.audio.clips.find((c) => c.id == clipId);
if (!clip || !clip.buffer) return; if (!clip || !clip.buffer) return;
const handleType = handle.classList.contains("left") ? "left" : "right"; const handleType = handle.classList.contains("left") ? "left" : "right";
const initialMouseX = e.clientX; const initialMouseX = e.clientX;
const secondsPerStep = getSecondsPerStep(); const secondsPerStep = getSecondsPerStep();
@ -1067,10 +859,9 @@ 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 = const initialOriginalDuration = clip.originalDuration || clip.buffer.duration;
clip.originalDuration || clip.buffer.duration;
const bufferStartTime = initialStartTime - initialOffset; const bufferStartTime = initialStartTime - initialOffset;
const onMouseMove = (moveEvent) => { const onMouseMove = (moveEvent) => {
const deltaX = moveEvent.clientX - initialMouseX; const deltaX = moveEvent.clientX - initialMouseX;
if (appState.global.resizeMode === "trim") { if (appState.global.resizeMode === "trim") {
@ -1081,125 +872,71 @@ 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 = `${ clipElement.style.width = `${(newEndTime - initialStartTime) * currentPixelsPerSecond}px`;
(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 = const minStartTime = initialStartTime + initialDuration - secondsPerStep;
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 = const newWidthFinalPx = (initialStartTime + initialDuration - newStartTime) * currentPixelsPerSecond;
(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 = `${ clipElement.style.width = `${(newEndTime - initialStartTime) * currentPixelsPerSecond}px`;
(newEndTime - initialStartTime) * currentPixelsPerSecond } else if (handleType === "left") {
}px`; let newLeftPx = initialLeftPx + deltaX;
} else if (handleType === "left") { let newStartTime = newLeftPx / currentPixelsPerSecond;
let newLeftPx = initialLeftPx + deltaX; newStartTime = quantizeTime(newStartTime);
let newStartTime = newLeftPx / currentPixelsPerSecond; const minStartTime = initialStartTime + initialDuration - secondsPerStep;
newStartTime = quantizeTime(newStartTime); newStartTime = Math.min(newStartTime, minStartTime);
const minStartTime = const newLeftFinalPx = newStartTime * currentPixelsPerSecond;
initialStartTime + initialDuration - secondsPerStep; const newWidthFinalPx = (initialStartTime + initialDuration - newStartTime) * currentPixelsPerSecond;
newStartTime = Math.min(newStartTime, minStartTime); clipElement.style.left = `${newLeftFinalPx}px`;
const newLeftFinalPx = newStartTime * currentPixelsPerSecond; clipElement.style.width = `${newWidthFinalPx}px`;
const newWidthFinalPx = }
(initialStartTime + initialDuration - newStartTime) *
currentPixelsPerSecond;
clipElement.style.left = `${newLeftFinalPx}px`;
clipElement.style.width = `${newWidthFinalPx}px`;
}
} }
}; };
const onMouseUp = () => { const onMouseUp = (upEvent) => {
document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp); document.removeEventListener("mouseup", onMouseUp);
const finalLeftPx = clipElement.offsetLeft; const finalLeftPx = clipElement.offsetLeft;
const finalWidthPx = clipElement.offsetWidth; const finalWidthPx = clipElement.offsetWidth;
const newStartTime = finalLeftPx / currentPixelsPerSecond; const newStartTime = finalLeftPx / currentPixelsPerSecond;
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, { updateAudioClipProperties(clipId, { durationInSeconds: newDuration, pitch: 0 });
durationInSeconds: newDuration, sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { durationInSeconds: newDuration, pitch: 0 } });
pitch: 0, } else {
}); updateAudioClipProperties(clipId, { startTimeInSeconds: newStartTime, durationInSeconds: newDuration, offset: newOffset, pitch: 0 });
sendActionSafe({ sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { startTimeInSeconds: newStartTime, durationInSeconds: newDuration, offset: newOffset, pitch: 0 } });
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, { updateAudioClipProperties(clipId, { durationInSeconds: newDuration, pitch: newPitch, offset: 0 });
durationInSeconds: newDuration, sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { durationInSeconds: newDuration, pitch: newPitch, offset: 0 } });
pitch: newPitch, } else {
offset: 0, 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 } });
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;
@ -1209,9 +946,8 @@ 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;
const clipModel = appState.audio.clips.find( // 🔑 pega o clip no estado pra ter o tempo inicial real
(c) => String(c.id) === String(clipId) const clipModel = appState.audio.clips.find(c => String(c.id) === String(clipId));
);
const initialStartTime = Number(clipModel?.startTimeInSeconds || 0); const initialStartTime = Number(clipModel?.startTimeInSeconds || 0);
e.preventDefault(); e.preventDefault();
@ -1225,13 +961,8 @@ 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( const overElement = document.elementFromPoint(moveEvent.clientX, moveEvent.clientY);
moveEvent.clientX, const overLane = overElement ? overElement.closest(".audio-track-lane") : null;
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");
@ -1251,24 +982,15 @@ export function renderAudioEditor() {
const newTrackId = finalLane.dataset.trackId; const newTrackId = finalLane.dataset.trackId;
const deltaX = // ✅ delta do mouse + delta de scroll durante o drag (se houver)
(upEvent.clientX - initialMouseX) + const deltaX = (upEvent.clientX - initialMouseX) + (scrollEl.scrollLeft - initialScrollLeft);
(scrollEl.scrollLeft - initialScrollLeft);
let newStartTime = let newStartTime = initialStartTime + (deltaX / currentPixelsPerSecond);
initialStartTime + deltaX / currentPixelsPerSecond;
newStartTime = Math.max(0, newStartTime); newStartTime = Math.max(0, newStartTime);
newStartTime = quantizeTime(newStartTime); newStartTime = quantizeTime(newStartTime);
updateAudioClipProperties(clipId, { updateAudioClipProperties(clipId, { trackId: newTrackId, startTimeInSeconds: newStartTime });
trackId: newTrackId, sendActionSafe({ type: "UPDATE_AUDIO_CLIP", clipId, props: { trackId: newTrackId, startTimeInSeconds: newStartTime } });
startTimeInSeconds: newStartTime,
});
sendActionSafe({
type: "UPDATE_AUDIO_CLIP",
clipId,
props: { trackId: newTrackId, startTimeInSeconds: newStartTime },
});
renderAudioEditor(); renderAudioEditor();
}; };
@ -1277,6 +999,7 @@ 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) {
@ -1289,7 +1012,7 @@ export function renderAudioEditor() {
const newTime = absoluteX / currentPixelsPerSecond; const newTime = absoluteX / currentPixelsPerSecond;
sendAction({ type: "SET_SEEK_TIME", seekTime: newTime }); sendAction({ type: "SET_SEEK_TIME", seekTime: newTime });
}; };
handleSeek(e); handleSeek(e);
const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent); const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent);
const onMouseUpSeek = () => { const onMouseUpSeek = () => {
document.removeEventListener("mousemove", onMouseMoveSeek); document.removeEventListener("mousemove", onMouseMoveSeek);
@ -1374,22 +1097,8 @@ export function renderAudioEditor() {
} }
} }
}); });
// ✅ Restaura o scroll anterior após reconstruir o container
// (evita “voltar pro início” depois de mover/redimensionar/deletar)
try {
newTrackContainer.scrollLeft = prevScrollLeft;
newTrackContainer.scrollTop = prevScrollTop;
// mantém régua alinhada (caso ela suporte scrollLeft)
const mainRuler = tracksParent.querySelector(".timeline-ruler");
if (mainRuler) mainRuler.scrollLeft = prevScrollLeft;
} catch (err) {
// silencioso: não pode quebrar a DAW
}
} }
export function updateAudioEditorUI() { export function updateAudioEditorUI() {
const playBtn = document.getElementById("audio-editor-play-btn"); const playBtn = document.getElementById("audio-editor-play-btn");
if (!playBtn) return; if (!playBtn) return;