Add 32-bar timeline header above the matrix grid in #tab-matrix. - Tap a bar: sends /matrix/seek <bar> - Drag across bars: sends /matrix/loop <min> <max> - BOUCLE COMPLETE button: resets to /matrix/loop 0 31 - .in-loop highlight from /matrix/loop feedback - .tl-head moves with /matrix/playhead (O(32)) - Pointer Events API (pointerdown/move/up + setPointerCapture + elementFromPoint) — works on desktop mouse and iPad touch
This commit is contained in:
@@ -129,3 +129,16 @@ button.queued { animation: queued-blink 600ms ease-in-out infinite alternate; }
|
||||
.mat-action-btn { width: auto; padding: 8px 14px; font-size: 14px; flex-shrink: 0; }
|
||||
.mat-refresh-btn { width: auto; padding: 8px 12px; font-size: 16px; flex-shrink: 0;
|
||||
background: #1a2a1a; border-color: #2a4a2a; color: #4d9; }
|
||||
.mat-loop-full-btn { width: auto; padding: 8px 14px; font-size: 13px; margin: 4px 0;
|
||||
background: #1a1a2e; border-color: #335; color: #9af; }
|
||||
|
||||
/* --- Matrix timeline header --- */
|
||||
.matrix-timeline { display: flex; gap: 1px; margin: 2px 0; touch-action: none; }
|
||||
.tl-spacer { width: 54px; flex-shrink: 0; }
|
||||
.tl-bar { width: 22px; min-width: 22px; height: 20px; flex-shrink: 0;
|
||||
background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 2px;
|
||||
cursor: pointer; font-size: 9px; color: #555; text-align: center;
|
||||
line-height: 20px; user-select: none; -webkit-user-select: none; }
|
||||
.tl-bar.in-loop { background: #1a2a1a; border-color: #2a5a2a; color: #4d9; }
|
||||
.tl-bar.tl-head { background: #fa0; border-color: #fc0; color: #000; }
|
||||
.tl-bar.tl-head.in-loop { background: #fa0; border-color: #fc0; color: #000; }
|
||||
|
||||
@@ -143,6 +143,19 @@ ws.addEventListener("message", (ev) => {
|
||||
const el = cellRefs[matPlayhead][vi];
|
||||
if (el) el.classList.add("playing");
|
||||
}
|
||||
updateTimelineHead(bar);
|
||||
return;
|
||||
}
|
||||
|
||||
// /matrix/loop <start> <end> — loop region feedback
|
||||
if (address === "/matrix/loop") {
|
||||
const s = Math.round(Number(args[0]));
|
||||
const e = Math.round(Number(args[1]));
|
||||
if (Number.isFinite(s) && Number.isFinite(e) && s >= 0 && e < MATRIX_BARS) {
|
||||
matLoopStart = Math.min(s, e);
|
||||
matLoopEnd = Math.max(s, e);
|
||||
updateTimelineLoop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -211,6 +224,8 @@ let matGrid = Array.from({ length: 16 }, () => new Array(32).fill(0));
|
||||
// cellRefs[bar][vi] for O(16) playhead column toggle
|
||||
const cellRefs = Array.from({ length: 32 }, () => new Array(16).fill(null));
|
||||
let matPlayhead = -1;
|
||||
let matLoopStart = 0;
|
||||
let matLoopEnd = 31;
|
||||
|
||||
function loadMatState() {
|
||||
try {
|
||||
@@ -291,6 +306,107 @@ function renderMatrix() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Matrix timeline: 32-bar loop region + playhead ---
|
||||
function renderTimeline() {
|
||||
const tl = document.getElementById("matrix-timeline");
|
||||
if (!tl) return;
|
||||
tl.innerHTML = "";
|
||||
const spacer = document.createElement("span");
|
||||
spacer.className = "tl-spacer";
|
||||
tl.appendChild(spacer);
|
||||
for (let bar = 0; bar < MATRIX_BARS; bar++) {
|
||||
const cell = document.createElement("span");
|
||||
cell.className = "tl-bar";
|
||||
cell.dataset.bar = bar;
|
||||
if (bar % 4 === 0) cell.textContent = String(bar + 1);
|
||||
if (bar >= matLoopStart && bar <= matLoopEnd) cell.classList.add("in-loop");
|
||||
if (bar === matPlayhead) cell.classList.add("tl-head");
|
||||
tl.appendChild(cell);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTimelineLoop() {
|
||||
const tl = document.getElementById("matrix-timeline");
|
||||
if (!tl) return;
|
||||
tl.querySelectorAll(".tl-bar").forEach((cell) => {
|
||||
const b = +cell.dataset.bar;
|
||||
cell.classList.toggle("in-loop", b >= matLoopStart && b <= matLoopEnd);
|
||||
});
|
||||
}
|
||||
|
||||
function updateTimelineHead(bar) {
|
||||
const tl = document.getElementById("matrix-timeline");
|
||||
if (!tl) return;
|
||||
const prev = tl.querySelector(".tl-head");
|
||||
if (prev) prev.classList.remove("tl-head");
|
||||
const next = tl.querySelector(`.tl-bar[data-bar="${bar}"]`);
|
||||
if (next) next.classList.add("tl-head");
|
||||
}
|
||||
|
||||
function initTimelinePointer() {
|
||||
const tl = document.getElementById("matrix-timeline");
|
||||
if (!tl) return;
|
||||
let dragStart = -1;
|
||||
let dragging = false;
|
||||
|
||||
function barFromPoint(x, y) {
|
||||
const el = document.elementFromPoint(x, y);
|
||||
if (!el) return -1;
|
||||
const barEl = el.classList && el.classList.contains("tl-bar")
|
||||
? el
|
||||
: (el.closest ? el.closest(".tl-bar") : null);
|
||||
if (!barEl) return -1;
|
||||
const b = +barEl.dataset.bar;
|
||||
return (Number.isFinite(b) && b >= 0 && b < MATRIX_BARS) ? b : -1;
|
||||
}
|
||||
|
||||
function previewLoop(start, end) {
|
||||
const lo = Math.min(start, end);
|
||||
const hi = Math.max(start, end);
|
||||
tl.querySelectorAll(".tl-bar").forEach((cell) => {
|
||||
const b = +cell.dataset.bar;
|
||||
cell.classList.toggle("in-loop", b >= lo && b <= hi);
|
||||
});
|
||||
}
|
||||
|
||||
tl.addEventListener("pointerdown", (e) => {
|
||||
const bar = barFromPoint(e.clientX, e.clientY);
|
||||
if (bar < 0) return;
|
||||
e.preventDefault();
|
||||
tl.setPointerCapture(e.pointerId);
|
||||
dragStart = bar;
|
||||
dragging = true;
|
||||
});
|
||||
|
||||
tl.addEventListener("pointermove", (e) => {
|
||||
if (!dragging) return;
|
||||
const cur = barFromPoint(e.clientX, e.clientY);
|
||||
if (cur >= 0) previewLoop(dragStart, cur);
|
||||
});
|
||||
|
||||
tl.addEventListener("pointerup", (e) => {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
tl.releasePointerCapture(e.pointerId);
|
||||
const end = barFromPoint(e.clientX, e.clientY);
|
||||
const validEnd = end >= 0 ? end : dragStart;
|
||||
if (validEnd === dragStart) {
|
||||
send("/matrix/seek", dragStart);
|
||||
} else {
|
||||
const lo = Math.min(dragStart, validEnd);
|
||||
const hi = Math.max(dragStart, validEnd);
|
||||
send("/matrix/loop", lo, hi);
|
||||
}
|
||||
dragStart = -1;
|
||||
});
|
||||
|
||||
tl.addEventListener("pointercancel", () => {
|
||||
dragging = false;
|
||||
dragStart = -1;
|
||||
updateTimelineLoop();
|
||||
});
|
||||
}
|
||||
|
||||
// --- Matrix audio-reactive glow (trigger envelope; math in matrix_glow.js) ---
|
||||
const voiceLevel = new Array(MATRIX_VOICES.length).fill(0);
|
||||
let glowRaf = null;
|
||||
@@ -516,6 +632,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
renderRhyGrid();
|
||||
renderMelSteps();
|
||||
renderMatrix();
|
||||
renderTimeline();
|
||||
initTimelinePointer();
|
||||
|
||||
// Name input: rename selected preset and update button labels
|
||||
const melName = document.getElementById("mel-name");
|
||||
@@ -619,6 +737,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const sceneNext = document.getElementById("scene-next");
|
||||
if (sceneNext) sceneNext.addEventListener("click", () => send("/scene/next"));
|
||||
|
||||
// Matrix loop full reset
|
||||
const matLoopFull = document.getElementById("matrix-loop-full");
|
||||
if (matLoopFull) matLoopFull.addEventListener("click", () => send("/matrix/loop", 0, MATRIX_BARS - 1));
|
||||
|
||||
// Matrix transport
|
||||
const matPlay = document.getElementById("matrix-play");
|
||||
if (matPlay) matPlay.addEventListener("click", () => send("/matrix/play"));
|
||||
|
||||
@@ -165,7 +165,9 @@
|
||||
<button id="matrix-load" class="mat-action-btn">CHARGER</button>
|
||||
<button id="matrix-refresh" class="mat-refresh-btn" title="Rafraichir la liste">↻</button>
|
||||
</div>
|
||||
<button id="matrix-loop-full" class="mat-action-btn mat-loop-full-btn">BOUCLE COMPLETE</button>
|
||||
<div class="matrix-scroll">
|
||||
<div id="matrix-timeline" class="matrix-timeline"></div>
|
||||
<div id="matrix-grid"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user