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;
// ✅ Salva o scroll atual (senão toda edição “pula” pro início)
const prevScrollLeft = existingTrackContainer.scrollLeft || 0;
const prevScrollTop = existingTrackContainer.scrollTop || 0;
_ensureGlobalPlaylistSelectionFields();
_installPlaylistKeybindOnce();
@ -808,47 +804,22 @@ 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;
}
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
.querySelectorAll(".bassline-clip.selected")
.querySelectorAll(".timeline-clip.selected")
.forEach((c) => c.classList.remove("selected"));
}
}
// Slice Tool (áudio apenas)
if (
appState.global.sliceToolActive &&
clipElement &&
!clipElement.classList.contains("bassline-clip")
) {
const currentPixelsPerSecond = getPixelsPerSecond();
const handle = e.target.closest(".clip-resize-handle");
// Slice Tool
if (appState.global.sliceToolActive && clipElement && !clipElement.classList.contains("bassline-clip")) {
e.preventDefault();
e.stopPropagation();
const clipId = clipElement.dataset.clipId;
@ -872,193 +843,14 @@ export function renderAudioEditor() {
return;
}
// =========================================================
// ✅ 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)
// =========================================================
// Resize Handle
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();
@ -1067,10 +859,9 @@ 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") {
@ -1081,125 +872,71 @@ 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 = () => {
const onMouseUp = (upEvent) => {
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;
@ -1209,9 +946,8 @@ export function renderAudioEditor() {
if (clipElement && !clipElement.classList.contains("bassline-clip")) {
const clipId = clipElement.dataset.clipId;
const clipModel = appState.audio.clips.find(
(c) => String(c.id) === String(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 initialStartTime = Number(clipModel?.startTimeInSeconds || 0);
e.preventDefault();
@ -1225,13 +961,8 @@ 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");
@ -1251,24 +982,15 @@ export function renderAudioEditor() {
const newTrackId = finalLane.dataset.trackId;
const deltaX =
(upEvent.clientX - initialMouseX) +
(scrollEl.scrollLeft - initialScrollLeft);
// ✅ delta do mouse + delta de scroll durante o drag (se houver)
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();
};
@ -1277,6 +999,7 @@ export function renderAudioEditor() {
return;
}
// Seek na Pista
const timelineContainer = e.target.closest(".timeline-container");
if (timelineContainer) {
@ -1289,7 +1012,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);
@ -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() {
const playBtn = document.getElementById("audio-editor-play-btn");
if (!playBtn) return;