refactor: extract matrix grid and voice editor
CI build oscope-of / build-check (push) Has been cancelled
CI build oscope-of / build-check (push) Has been cancelled
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
import { send, on, onOpen, onClose } from "./osc.js";
|
||||
import {
|
||||
PALETTE,
|
||||
MATRIX_VOICES, MATRIX_BARS, MATRIX_INST_CHOICES,
|
||||
MATRIX_MOD_SOURCES, MATRIX_MOD_TARGETS,
|
||||
mkColor, matGrid, matInst, matColorDefs,
|
||||
PALETTE, MATRIX_BARS,
|
||||
resetMatGrid, saveMatState, loadMatState,
|
||||
} from "./matrix-state.js";
|
||||
import { init as initMixer } from "./mixer.js";
|
||||
@@ -11,6 +8,8 @@ import { init as initTransport } from "./transport.js";
|
||||
import { init as initScenes } from "./scenes.js";
|
||||
import { init as initSequencer } from "./sequencer.js";
|
||||
import { init as initLaunchpad } from "./launchpad.js";
|
||||
import { init as initGrid, renderMatrix } from "./matrix-grid.js";
|
||||
import { init as initVoiceEditor } from "./voice-editor.js";
|
||||
|
||||
function installPaletteVars() {
|
||||
const root = document.documentElement.style;
|
||||
@@ -21,458 +20,6 @@ const dot = () => document.getElementById("status");
|
||||
onOpen(() => { dot().className = "on"; send("/matrix/list"); });
|
||||
onClose(() => { dot().className = "off"; });
|
||||
|
||||
// --- Matrix (22-voice x 64-bar song arranger) ---
|
||||
// Data layer lives in matrix-state.js — constants/state/save/load imported above.
|
||||
let cdEditVoice = -1, cdColor = 1, cdAuditioning = false;
|
||||
// cellRefs[bar][vi] for O(16) playhead column toggle
|
||||
const cellRefs = Array.from({ length: MATRIX_BARS }, () => new Array(MATRIX_VOICES.length).fill(null));
|
||||
let matPlayhead = -1;
|
||||
let matLoopStart = 0;
|
||||
let matLoopEnd = MATRIX_BARS - 1;
|
||||
|
||||
function applyMatCellColor(el, color) {
|
||||
for (let c = 0; c <= 6; c++) el.classList.remove("m" + c);
|
||||
el.classList.add("m" + color);
|
||||
el.dataset.state = color;
|
||||
}
|
||||
|
||||
function renderMatrix() {
|
||||
const container = document.getElementById("matrix-grid");
|
||||
if (!container) return;
|
||||
container.innerHTML = "";
|
||||
|
||||
// Header row: bar numbers every 4 bars
|
||||
const headerRow = document.createElement("div");
|
||||
headerRow.className = "mrow";
|
||||
const corner = document.createElement("span");
|
||||
corner.className = "mvoice-label";
|
||||
headerRow.appendChild(corner);
|
||||
const ih = document.createElement("span");
|
||||
ih.className = "minst-head";
|
||||
headerRow.appendChild(ih);
|
||||
const editHead = document.createElement("span");
|
||||
editHead.className = "minst-edit-head";
|
||||
headerRow.appendChild(editHead);
|
||||
for (let bar = 0; bar < MATRIX_BARS; bar++) {
|
||||
const num = document.createElement("span");
|
||||
num.className = "mbar-num";
|
||||
num.textContent = (bar % 4 === 0) ? String(bar + 1) : "";
|
||||
headerRow.appendChild(num);
|
||||
}
|
||||
container.appendChild(headerRow);
|
||||
|
||||
// Voice rows
|
||||
for (let vi = 0; vi < MATRIX_VOICES.length; vi++) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "mrow";
|
||||
const label = document.createElement("span");
|
||||
label.className = "mvoice-label";
|
||||
label.textContent = MATRIX_VOICES[vi];
|
||||
row.appendChild(label);
|
||||
const vinst = document.createElement("select");
|
||||
vinst.className = "minst"; vinst.dataset.vi = vi;
|
||||
[["default","default"]].concat((MATRIX_INST_CHOICES[MATRIX_VOICES[vi]]||[]).map(s=>[s,s]))
|
||||
.forEach(([val,txt])=>{const o=document.createElement("option");o.value=val;o.textContent=txt;vinst.appendChild(o);});
|
||||
vinst.value = matInst[vi] || "default";
|
||||
vinst.addEventListener("change", ()=>{ matInst[vi]=vinst.value; saveMatState(); send("/matrix/instrument", vi, vinst.value); });
|
||||
row.appendChild(vinst);
|
||||
const edit = document.createElement("button");
|
||||
edit.className = "minst-edit"; edit.textContent = "..."; edit.dataset.vi = vi;
|
||||
edit.addEventListener("click", () => openColorDef(vi));
|
||||
row.appendChild(edit);
|
||||
for (let bar = 0; bar < MATRIX_BARS; bar++) {
|
||||
const color = matGrid[vi][bar];
|
||||
const cell = document.createElement("button");
|
||||
cell.className = "mcell m" + color;
|
||||
cell.dataset.vi = vi;
|
||||
cell.dataset.bar = bar;
|
||||
cell.dataset.state = color;
|
||||
if (bar === matPlayhead) cell.classList.add("playing");
|
||||
cell.addEventListener("click", () => {
|
||||
const cur = +cell.dataset.state;
|
||||
const next = (cur + 1) % 7;
|
||||
matGrid[vi][bar] = next;
|
||||
saveMatState();
|
||||
applyMatCellColor(cell, next);
|
||||
if (bar === matPlayhead) cell.classList.add("playing");
|
||||
send("/matrix/cell", vi, bar, next);
|
||||
});
|
||||
cell.addEventListener("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
matGrid[vi][bar] = 0; saveMatState();
|
||||
applyMatCellColor(cell, 0);
|
||||
if (bar === matPlayhead) cell.classList.add("playing");
|
||||
send("/matrix/cell", vi, bar, 0);
|
||||
});
|
||||
cellRefs[bar][vi] = cell;
|
||||
row.appendChild(cell);
|
||||
}
|
||||
container.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Color def modal ---
|
||||
function renderColorSel(vi) {
|
||||
const sel = document.getElementById("cd-colorsel");
|
||||
if (!sel) return;
|
||||
sel.innerHTML = "";
|
||||
for (let c = 1; c <= 6; c++) {
|
||||
const btn = document.createElement("button");
|
||||
btn.textContent = String(c);
|
||||
btn.style.background = PALETTE[c-1];
|
||||
btn.style.borderColor = PALETTE[c-1];
|
||||
btn.classList.toggle("on", c === cdColor);
|
||||
btn.addEventListener("click", () => { cdColor = c; renderColorSel(vi); renderPattern(vi); });
|
||||
sel.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPattern(vi) {
|
||||
const p = document.getElementById("cd-pattern");
|
||||
if (!p) return;
|
||||
p.innerHTML = "";
|
||||
const c = cdColor;
|
||||
const d = matColorDefs[vi][c];
|
||||
if (!d) return;
|
||||
if (!d.steps) d.steps = new Array(16).fill(null);
|
||||
if (!d.pose) d.pose = [];
|
||||
if (d.mod === undefined) d.mod = null;
|
||||
|
||||
const targets = MATRIX_MOD_TARGETS[MATRIX_VOICES[vi]] || ["none","amp"];
|
||||
const hasCut = targets.includes("cutoff"), hasPan = targets.includes("pan");
|
||||
const mk = (label, el) => { const w = document.createElement("label");
|
||||
w.className = "cd-field"; w.textContent = label; w.appendChild(el); return w; };
|
||||
|
||||
// Instrument + variation row
|
||||
const varRow = document.createElement("div"); varRow.className = "cd-row m" + c;
|
||||
const inst = document.createElement("select");
|
||||
[["default","default"]].concat((MATRIX_INST_CHOICES[MATRIX_VOICES[vi]]||[]).map(s=>[s,s]))
|
||||
.forEach(([val,txt])=>{ const o=document.createElement("option"); o.value=val; o.textContent=txt; inst.appendChild(o); });
|
||||
inst.value = d.inst || "default";
|
||||
inst.addEventListener("change", () => setCD(c, "inst", inst.value));
|
||||
const stretch = document.createElement("select");
|
||||
[0.5,1.0,2.0].forEach(v=>{ const o=document.createElement("option"); o.value=v; o.textContent=v+"x"; stretch.appendChild(o); });
|
||||
stretch.value = d.stretch; stretch.addEventListener("change", () => setCD(c, "stretch", +stretch.value));
|
||||
const oct = document.createElement("select");
|
||||
[-1,0,1].forEach(v=>{ const o=document.createElement("option"); o.value=v; o.textContent=v; oct.appendChild(o); });
|
||||
oct.value = d.octave; oct.addEventListener("change", () => setCD(c, "octave", +oct.value));
|
||||
const amp = document.createElement("input"); amp.type="range"; amp.min=0; amp.max=1.5; amp.step=0.01;
|
||||
amp.value = d.amp; amp.addEventListener("change", () => setCD(c, "amp", +amp.value));
|
||||
varRow.appendChild(mk("inst", inst)); varRow.appendChild(mk("stretch", stretch));
|
||||
varRow.appendChild(mk("oct", oct)); varRow.appendChild(mk("amp", amp));
|
||||
if (hasCut) { const cut = document.createElement("input"); cut.type="range"; cut.min=200; cut.max=6000; cut.step=10;
|
||||
cut.value = d.cutoff > 0 ? d.cutoff : 1000;
|
||||
cut.addEventListener("change", () => setCD(c, "cutoff", +cut.value)); varRow.appendChild(mk("cutoff", cut)); }
|
||||
if (hasPan) { const pan = document.createElement("input"); pan.type="range"; pan.min=-1; pan.max=1; pan.step=0.01;
|
||||
pan.value = d.pan >= -1 ? d.pan : 0;
|
||||
pan.addEventListener("change", () => setCD(c, "pan", +pan.value)); varRow.appendChild(mk("pan", pan)); }
|
||||
p.appendChild(varRow);
|
||||
|
||||
// 16-step row
|
||||
const seqRow = document.createElement("div"); seqRow.className = "seq-row m" + c;
|
||||
for (let s = 0; s < 16; s++) {
|
||||
const cell = document.createElement("button"); cell.className = "seq-cell";
|
||||
const st = d.steps[s];
|
||||
cell.textContent = st ? st.degree : "";
|
||||
if (st) cell.classList.add("on");
|
||||
cell.addEventListener("click", () => {
|
||||
if (d.steps[s]) { d.steps[s] = null; send("/matrix/step", vi, c, s, -99, 0); }
|
||||
else { d.steps[s] = {degree:0, vel:0.8}; send("/matrix/step", vi, c, s, 0, 0.8); }
|
||||
saveMatState(); renderPattern(vi);
|
||||
});
|
||||
cell.addEventListener("wheel", (e) => {
|
||||
e.preventDefault(); const st2 = d.steps[s]; if (!st2) return;
|
||||
if (e.shiftKey) {
|
||||
st2.vel = Math.max(0, Math.min(1, st2.vel + (e.deltaY < 0 ? 0.1 : -0.1)));
|
||||
} else {
|
||||
st2.degree = Math.max(-48, Math.min(48, st2.degree + (e.deltaY < 0 ? 1 : -1)));
|
||||
}
|
||||
send("/matrix/step", vi, c, s, st2.degree, st2.vel); saveMatState(); renderPattern(vi);
|
||||
});
|
||||
seqRow.appendChild(cell);
|
||||
}
|
||||
p.appendChild(seqRow);
|
||||
|
||||
// Effect (mod) row
|
||||
const modDiv = document.createElement("div"); modDiv.className = "cd-row m" + c;
|
||||
const src = document.createElement("select");
|
||||
MATRIX_MOD_SOURCES.forEach(s=>{ const o=document.createElement("option"); o.value=s; o.textContent=s; src.appendChild(o); });
|
||||
const tgt = document.createElement("select");
|
||||
targets.forEach(t=>{ const o=document.createElement("option"); o.value=t; o.textContent=t; tgt.appendChild(o); });
|
||||
const dep = document.createElement("input"); dep.type="range"; dep.min=0; dep.max=1; dep.step=0.01;
|
||||
const mod = d.mod || {source:"none", target:"none", depth:0.5};
|
||||
src.value = mod.source; tgt.value = mod.target; dep.value = mod.depth;
|
||||
const sendMod = () => {
|
||||
matColorDefs[vi][c].mod = {source:src.value, target:tgt.value, depth:+dep.value};
|
||||
saveMatState(); send("/matrix/colormod", vi, c, src.value, tgt.value, +dep.value);
|
||||
};
|
||||
src.addEventListener("change", sendMod); tgt.addEventListener("change", sendMod); dep.addEventListener("change", sendMod);
|
||||
modDiv.appendChild(mk("src", src)); modDiv.appendChild(mk("tgt", tgt)); modDiv.appendChild(mk("depth", dep));
|
||||
p.appendChild(modDiv);
|
||||
|
||||
// Pose section
|
||||
const poseDiv = document.createElement("div");
|
||||
const addDiv = document.createElement("div");
|
||||
const poseId = document.createElement("input"); poseId.placeholder = "poseId"; poseId.className = "pose-id";
|
||||
const poseAct = document.createElement("select");
|
||||
["trigger","gate"].forEach(a=>{ const o=document.createElement("option"); o.value=a; o.textContent=a; poseAct.appendChild(o); });
|
||||
const poseGo = document.createElement("button"); poseGo.textContent = "+";
|
||||
poseGo.addEventListener("click", () => {
|
||||
if (!poseId.value) return;
|
||||
const poses = (d.pose || []).filter(b => b.poseId !== poseId.value);
|
||||
poses.push({poseId: poseId.value, action: poseAct.value});
|
||||
matColorDefs[vi][c].pose = poses;
|
||||
saveMatState(); send("/matrix/colorpose", vi, c, poses.length, ...poses.flatMap(b => [b.poseId, b.action]));
|
||||
renderPattern(vi);
|
||||
});
|
||||
[poseId, poseAct, poseGo].forEach(e => addDiv.appendChild(e));
|
||||
poseDiv.appendChild(addDiv);
|
||||
(d.pose || []).forEach(b => {
|
||||
const line = document.createElement("div");
|
||||
line.textContent = `${b.poseId} -> ${b.action}`;
|
||||
const del = document.createElement("button"); del.textContent = "x";
|
||||
del.style.cssText = "width:auto;padding:2px 6px;font-size:10px;";
|
||||
del.addEventListener("click", () => {
|
||||
matColorDefs[vi][c].pose = (d.pose || []).filter(pp => pp.poseId !== b.poseId);
|
||||
const poses = matColorDefs[vi][c].pose;
|
||||
saveMatState(); send("/matrix/colorpose", vi, c, poses.length, ...poses.flatMap(pp => [pp.poseId, pp.action]));
|
||||
renderPattern(vi);
|
||||
});
|
||||
line.appendChild(del); poseDiv.appendChild(line);
|
||||
});
|
||||
p.appendChild(poseDiv);
|
||||
}
|
||||
|
||||
function openColorDef(vi) {
|
||||
cdEditVoice = vi;
|
||||
cdColor = 1;
|
||||
document.getElementById("cd-title").textContent = MATRIX_VOICES[vi];
|
||||
document.getElementById("cd-audition").classList.toggle("on", cdAuditioning);
|
||||
renderColorSel(vi);
|
||||
renderPattern(vi);
|
||||
document.getElementById("colordef-modal").hidden = false;
|
||||
send("/matrix/colordefs/get", vi);
|
||||
send("/matrix/steps/get", vi);
|
||||
send("/matrix/colordefs/get/ext", vi);
|
||||
}
|
||||
|
||||
function setCD(color, field, value) {
|
||||
if (cdEditVoice < 0) return;
|
||||
matColorDefs[cdEditVoice][color][field] = value;
|
||||
saveMatState();
|
||||
send("/matrix/colordef", cdEditVoice, color, field, value);
|
||||
}
|
||||
|
||||
function closeColorDef() {
|
||||
if (cdAuditioning && cdEditVoice >= 0) { send("/matrix/audition", cdEditVoice, 0); cdAuditioning = false; }
|
||||
document.getElementById("cd-audition").classList.remove("on");
|
||||
document.getElementById("colordef-modal").hidden = true;
|
||||
cdEditVoice = -1;
|
||||
}
|
||||
|
||||
// --- Matrix timeline: 64-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 ---
|
||||
const voiceLevel = new Array(MATRIX_VOICES.length).fill(0);
|
||||
let glowRaf = null;
|
||||
let glowLastTs = 0;
|
||||
let glowAppliedBar = -1;
|
||||
|
||||
function clearGlowColumn(bar) {
|
||||
if (bar < 0) return;
|
||||
for (let vi = 0; vi < MATRIX_VOICES.length; vi++) {
|
||||
const el = cellRefs[bar][vi];
|
||||
if (el) el.style.removeProperty("--glow");
|
||||
}
|
||||
}
|
||||
|
||||
function glowFrame(ts) {
|
||||
const decay = window.MatrixGlow ? window.MatrixGlow.nextGlow : () => 0;
|
||||
const dt = glowLastTs ? (ts - glowLastTs) : (1000 / 60);
|
||||
glowLastTs = ts;
|
||||
if (matPlayhead !== glowAppliedBar) {
|
||||
clearGlowColumn(glowAppliedBar);
|
||||
glowAppliedBar = matPlayhead;
|
||||
}
|
||||
let anyActive = false;
|
||||
for (let vi = 0; vi < MATRIX_VOICES.length; vi++) {
|
||||
const lvl = decay(voiceLevel[vi], dt);
|
||||
voiceLevel[vi] = lvl;
|
||||
if (matPlayhead >= 0) {
|
||||
const el = cellRefs[matPlayhead][vi];
|
||||
if (el) {
|
||||
if (lvl > 0) el.style.setProperty("--glow", lvl.toFixed(3));
|
||||
else el.style.removeProperty("--glow");
|
||||
}
|
||||
}
|
||||
if (lvl > 0) anyActive = true;
|
||||
}
|
||||
if (anyActive) {
|
||||
glowRaf = requestAnimationFrame(glowFrame);
|
||||
} else {
|
||||
clearGlowColumn(glowAppliedBar);
|
||||
glowRaf = null;
|
||||
glowLastTs = 0;
|
||||
glowAppliedBar = -1;
|
||||
}
|
||||
}
|
||||
|
||||
function triggerGlow(vi, amp) {
|
||||
if (!Number.isInteger(vi) || vi < 0 || vi >= MATRIX_VOICES.length) return;
|
||||
const a = Math.max(0, Math.min(1, amp));
|
||||
if (a <= 0) return;
|
||||
voiceLevel[vi] = Math.max(voiceLevel[vi], a);
|
||||
if (glowRaf === null) {
|
||||
glowLastTs = 0;
|
||||
glowRaf = requestAnimationFrame(glowFrame);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Matrix OSC handlers ---
|
||||
// /matrix/playhead <bar>
|
||||
on("/matrix/playhead", (args) => {
|
||||
const bar = Math.round(Number(args[0]));
|
||||
if (!Number.isFinite(bar) || bar < 0 || bar >= MATRIX_BARS) return;
|
||||
if (matPlayhead >= 0) {
|
||||
for (let vi = 0; vi < MATRIX_VOICES.length; vi++) {
|
||||
const el = cellRefs[matPlayhead][vi];
|
||||
if (el) el.classList.remove("playing");
|
||||
}
|
||||
}
|
||||
matPlayhead = bar;
|
||||
for (let vi = 0; vi < MATRIX_VOICES.length; vi++) {
|
||||
const el = cellRefs[matPlayhead][vi];
|
||||
if (el) el.classList.add("playing");
|
||||
}
|
||||
updateTimelineHead(bar);
|
||||
});
|
||||
|
||||
// /matrix/loop <start> <end>
|
||||
on("/matrix/loop", (args) => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
// /matrix/cell <vi> <bar> <color>
|
||||
on("/matrix/cell", (args) => {
|
||||
const vi = Math.round(Number(args[0]));
|
||||
const bar = Math.round(Number(args[1]));
|
||||
const color = Math.round(Number(args[2]));
|
||||
if (vi < 0 || vi >= MATRIX_VOICES.length || bar < 0 || bar >= MATRIX_BARS || color < 0 || color > 6) return;
|
||||
matGrid[vi][bar] = color;
|
||||
saveMatState();
|
||||
const el = cellRefs[bar][vi];
|
||||
if (el) {
|
||||
applyMatCellColor(el, color);
|
||||
if (bar === matPlayhead) el.classList.add("playing");
|
||||
}
|
||||
});
|
||||
|
||||
// /matrix/list <name1> <name2> ...
|
||||
on("/matrix/list", (args) => {
|
||||
const sel = document.getElementById("matrix-list");
|
||||
if (!sel) return;
|
||||
@@ -487,95 +34,6 @@ on("/matrix/list", (args) => {
|
||||
if (args.map(String).includes(prev)) sel.value = prev;
|
||||
});
|
||||
|
||||
// /matrix/grid <N ints row-major vi*BARS+bar>
|
||||
on("/matrix/grid", (args) => {
|
||||
if (!Array.isArray(args) || args.length < (MATRIX_VOICES.length * MATRIX_BARS)) return;
|
||||
for (let vi = 0; vi < MATRIX_VOICES.length; vi++) {
|
||||
for (let bar = 0; bar < MATRIX_BARS; bar++) {
|
||||
const v = Math.round(Number(args[vi * MATRIX_BARS + bar]));
|
||||
matGrid[vi][bar] = (Number.isFinite(v) && v >= 0 && v <= 6) ? v : 0;
|
||||
}
|
||||
}
|
||||
saveMatState();
|
||||
renderMatrix();
|
||||
});
|
||||
|
||||
// /matrix/trig <vi> <amp>
|
||||
on("/matrix/trig", (args) => {
|
||||
triggerGlow(Math.round(Number(args[0])), Number(args[1]));
|
||||
});
|
||||
|
||||
// /matrix/instrument <vi> <inst>
|
||||
on("/matrix/instrument", (args) => {
|
||||
const vi = Math.round(Number(args[0])); const v = String(args[1] ?? "default");
|
||||
if (vi >= 0 && vi < MATRIX_VOICES.length) {
|
||||
matInst[vi] = v; saveMatState();
|
||||
const rowSel = document.querySelector(`.minst[data-vi="${vi}"]`);
|
||||
if (rowSel) rowSel.value = v;
|
||||
}
|
||||
});
|
||||
|
||||
// /matrix/instruments <N strings>
|
||||
on("/matrix/instruments", (args) => {
|
||||
for (let vi = 0; vi < MATRIX_VOICES.length && vi < args.length; vi++) {
|
||||
const v = String(args[vi] ?? "default"); matInst[vi] = v;
|
||||
const rowSel = document.querySelector(`.minst[data-vi="${vi}"]`);
|
||||
if (rowSel) rowSel.value = v;
|
||||
}
|
||||
saveMatState();
|
||||
});
|
||||
|
||||
on("/matrix/colordefs", (args) => {
|
||||
const vi = Math.round(Number(args[0]));
|
||||
if (vi >= 0 && vi < MATRIX_VOICES.length) {
|
||||
for (let c = 1; c <= 6; c++) {
|
||||
const b = 1 + (c - 1) * 6;
|
||||
const ex = matColorDefs[vi][c] || {};
|
||||
matColorDefs[vi][c] = { stretch:Number(args[b]), octave:Math.round(Number(args[b+1])),
|
||||
amp:Number(args[b+2]), inst:String(args[b+3] ?? "default"),
|
||||
cutoff:Number(args[b+4]), pan:Number(args[b+5]),
|
||||
mod:ex.mod ?? null, pose:ex.pose || [], steps:ex.steps || new Array(16).fill(null) };
|
||||
}
|
||||
saveMatState();
|
||||
if (cdEditVoice === vi && !document.getElementById("colordef-modal").hidden) {
|
||||
renderColorSel(vi); renderPattern(vi);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
on("/matrix/steps", (args) => {
|
||||
const vi = Math.round(Number(args[0])), c = Math.round(Number(args[1]));
|
||||
if (vi>=0 && vi<MATRIX_VOICES.length && c>=1 && c<=6 && matColorDefs[vi][c]) {
|
||||
if (!matColorDefs[vi][c].steps) matColorDefs[vi][c].steps = new Array(16).fill(null);
|
||||
for (let s=0; s<16; s++) {
|
||||
const deg = Number(args[2 + s*2]), vel = Number(args[3 + s*2]);
|
||||
matColorDefs[vi][c].steps[s] = (deg === -99) ? null : {degree:deg, vel:vel};
|
||||
}
|
||||
saveMatState();
|
||||
if (cdEditVoice===vi && cdColor===c && !document.getElementById("colordef-modal").hidden) renderPattern(vi);
|
||||
}
|
||||
});
|
||||
|
||||
on("/matrix/colormod", (args) => {
|
||||
const vi=Math.round(Number(args[0])), c=Math.round(Number(args[1]));
|
||||
if (vi>=0&&vi<MATRIX_VOICES.length&&c>=1&&c<=6&&matColorDefs[vi][c]) {
|
||||
const src=String(args[2]??"none");
|
||||
matColorDefs[vi][c].mod = (src==="none") ? null : {source:src, target:String(args[3]??"none"), depth:Number(args[4])||0};
|
||||
saveMatState();
|
||||
if (cdEditVoice===vi && cdColor===c && !document.getElementById("colordef-modal").hidden) renderPattern(vi);
|
||||
}
|
||||
});
|
||||
|
||||
on("/matrix/colorpose", (args) => {
|
||||
const vi=Math.round(Number(args[0])), c=Math.round(Number(args[1])), n=Math.round(Number(args[2]));
|
||||
if (vi>=0&&vi<MATRIX_VOICES.length&&c>=1&&c<=6&&matColorDefs[vi][c]) {
|
||||
matColorDefs[vi][c].pose = [];
|
||||
for (let k=0;k<n;k++){ const b=3+k*2; matColorDefs[vi][c].pose.push({poseId:String(args[b]),action:String(args[b+1])}); }
|
||||
saveMatState();
|
||||
if (cdEditVoice===vi && cdColor===c && !document.getElementById("colordef-modal").hidden) renderPattern(vi);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
installPaletteVars();
|
||||
loadMatState();
|
||||
@@ -597,19 +55,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
initScenes();
|
||||
initSequencer();
|
||||
initLaunchpad();
|
||||
|
||||
// Render matrix
|
||||
renderMatrix();
|
||||
renderTimeline();
|
||||
initTimelinePointer();
|
||||
|
||||
document.getElementById("cd-close").addEventListener("click", closeColorDef);
|
||||
document.getElementById("cd-audition").addEventListener("click", () => {
|
||||
if (cdEditVoice < 0) return;
|
||||
cdAuditioning = !cdAuditioning;
|
||||
send("/matrix/audition", cdEditVoice, cdAuditioning ? 1 : 0);
|
||||
document.getElementById("cd-audition").classList.toggle("on", cdAuditioning);
|
||||
});
|
||||
initVoiceEditor();
|
||||
initGrid();
|
||||
|
||||
// Generic OSC buttons
|
||||
document.querySelectorAll("[data-osc]").forEach((el) =>
|
||||
@@ -643,10 +90,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
b.addEventListener("click", () => send("/matrix/kit", b.dataset.kit));
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
if (cdAuditioning && cdEditVoice >= 0) send("/matrix/audition", cdEditVoice, 0);
|
||||
});
|
||||
|
||||
// Matrix persistence bar
|
||||
const matSave = document.getElementById("matrix-save");
|
||||
if (matSave) matSave.addEventListener("click", () => {
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
import { send, on } from "./osc.js";
|
||||
import {
|
||||
MATRIX_VOICES, MATRIX_BARS, MATRIX_INST_CHOICES,
|
||||
matGrid, matInst, saveMatState,
|
||||
} from "./matrix-state.js";
|
||||
import { openFor } from "./voice-editor.js";
|
||||
|
||||
// cellRefs[bar][vi] for O(1) playhead column toggle
|
||||
const cellRefs = Array.from({ length: MATRIX_BARS }, () => new Array(MATRIX_VOICES.length).fill(null));
|
||||
let matPlayhead = -1;
|
||||
let matLoopStart = 0;
|
||||
let matLoopEnd = MATRIX_BARS - 1;
|
||||
|
||||
function applyMatCellColor(el, color) {
|
||||
for (let c = 0; c <= 6; c++) el.classList.remove("m" + c);
|
||||
el.classList.add("m" + color);
|
||||
el.dataset.state = color;
|
||||
}
|
||||
|
||||
export function renderMatrix() {
|
||||
const container = document.getElementById("matrix-grid");
|
||||
if (!container) return;
|
||||
container.innerHTML = "";
|
||||
|
||||
// Header row: bar numbers every 4 bars
|
||||
const headerRow = document.createElement("div");
|
||||
headerRow.className = "mrow";
|
||||
const corner = document.createElement("span");
|
||||
corner.className = "mvoice-label";
|
||||
headerRow.appendChild(corner);
|
||||
const ih = document.createElement("span");
|
||||
ih.className = "minst-head";
|
||||
headerRow.appendChild(ih);
|
||||
const editHead = document.createElement("span");
|
||||
editHead.className = "minst-edit-head";
|
||||
headerRow.appendChild(editHead);
|
||||
for (let bar = 0; bar < MATRIX_BARS; bar++) {
|
||||
const num = document.createElement("span");
|
||||
num.className = "mbar-num";
|
||||
num.textContent = (bar % 4 === 0) ? String(bar + 1) : "";
|
||||
headerRow.appendChild(num);
|
||||
}
|
||||
container.appendChild(headerRow);
|
||||
|
||||
// Voice rows
|
||||
for (let vi = 0; vi < MATRIX_VOICES.length; vi++) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "mrow";
|
||||
const label = document.createElement("span");
|
||||
label.className = "mvoice-label";
|
||||
label.textContent = MATRIX_VOICES[vi];
|
||||
row.appendChild(label);
|
||||
const vinst = document.createElement("select");
|
||||
vinst.className = "minst"; vinst.dataset.vi = vi;
|
||||
[["default","default"]].concat((MATRIX_INST_CHOICES[MATRIX_VOICES[vi]]||[]).map(s=>[s,s]))
|
||||
.forEach(([val,txt])=>{const o=document.createElement("option");o.value=val;o.textContent=txt;vinst.appendChild(o);});
|
||||
vinst.value = matInst[vi] || "default";
|
||||
vinst.addEventListener("change", ()=>{ matInst[vi]=vinst.value; saveMatState(); send("/matrix/instrument", vi, vinst.value); });
|
||||
row.appendChild(vinst);
|
||||
const edit = document.createElement("button");
|
||||
edit.className = "minst-edit"; edit.textContent = "..."; edit.dataset.vi = vi;
|
||||
edit.addEventListener("click", () => openFor(vi));
|
||||
row.appendChild(edit);
|
||||
for (let bar = 0; bar < MATRIX_BARS; bar++) {
|
||||
const color = matGrid[vi][bar];
|
||||
const cell = document.createElement("button");
|
||||
cell.className = "mcell m" + color;
|
||||
cell.dataset.vi = vi;
|
||||
cell.dataset.bar = bar;
|
||||
cell.dataset.state = color;
|
||||
if (bar === matPlayhead) cell.classList.add("playing");
|
||||
cell.addEventListener("click", () => {
|
||||
const cur = +cell.dataset.state;
|
||||
const next = (cur + 1) % 7;
|
||||
matGrid[vi][bar] = next;
|
||||
saveMatState();
|
||||
applyMatCellColor(cell, next);
|
||||
if (bar === matPlayhead) cell.classList.add("playing");
|
||||
send("/matrix/cell", vi, bar, next);
|
||||
});
|
||||
cell.addEventListener("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
matGrid[vi][bar] = 0; saveMatState();
|
||||
applyMatCellColor(cell, 0);
|
||||
if (bar === matPlayhead) cell.classList.add("playing");
|
||||
send("/matrix/cell", vi, bar, 0);
|
||||
});
|
||||
cellRefs[bar][vi] = cell;
|
||||
row.appendChild(cell);
|
||||
}
|
||||
container.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Timeline ---
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
// --- Audio-reactive glow ---
|
||||
const voiceLevel = new Array(MATRIX_VOICES.length).fill(0);
|
||||
let glowRaf = null;
|
||||
let glowLastTs = 0;
|
||||
let glowAppliedBar = -1;
|
||||
|
||||
function clearGlowColumn(bar) {
|
||||
if (bar < 0) return;
|
||||
for (let vi = 0; vi < MATRIX_VOICES.length; vi++) {
|
||||
const el = cellRefs[bar][vi];
|
||||
if (el) el.style.removeProperty("--glow");
|
||||
}
|
||||
}
|
||||
|
||||
function glowFrame(ts) {
|
||||
const decay = window.MatrixGlow ? window.MatrixGlow.nextGlow : () => 0;
|
||||
const dt = glowLastTs ? (ts - glowLastTs) : (1000 / 60);
|
||||
glowLastTs = ts;
|
||||
if (matPlayhead !== glowAppliedBar) {
|
||||
clearGlowColumn(glowAppliedBar);
|
||||
glowAppliedBar = matPlayhead;
|
||||
}
|
||||
let anyActive = false;
|
||||
for (let vi = 0; vi < MATRIX_VOICES.length; vi++) {
|
||||
const lvl = decay(voiceLevel[vi], dt);
|
||||
voiceLevel[vi] = lvl;
|
||||
if (matPlayhead >= 0) {
|
||||
const el = cellRefs[matPlayhead][vi];
|
||||
if (el) {
|
||||
if (lvl > 0) el.style.setProperty("--glow", lvl.toFixed(3));
|
||||
else el.style.removeProperty("--glow");
|
||||
}
|
||||
}
|
||||
if (lvl > 0) anyActive = true;
|
||||
}
|
||||
if (anyActive) {
|
||||
glowRaf = requestAnimationFrame(glowFrame);
|
||||
} else {
|
||||
clearGlowColumn(glowAppliedBar);
|
||||
glowRaf = null;
|
||||
glowLastTs = 0;
|
||||
glowAppliedBar = -1;
|
||||
}
|
||||
}
|
||||
|
||||
export function triggerGlow(vi, amp) {
|
||||
if (!Number.isInteger(vi) || vi < 0 || vi >= MATRIX_VOICES.length) return;
|
||||
const a = Math.max(0, Math.min(1, amp));
|
||||
if (a <= 0) return;
|
||||
voiceLevel[vi] = Math.max(voiceLevel[vi], a);
|
||||
if (glowRaf === null) {
|
||||
glowLastTs = 0;
|
||||
glowRaf = requestAnimationFrame(glowFrame);
|
||||
}
|
||||
}
|
||||
|
||||
// --- OSC handlers (registered at module load) ---
|
||||
on("/matrix/playhead", (args) => {
|
||||
const bar = Math.round(Number(args[0]));
|
||||
if (!Number.isFinite(bar) || bar < 0 || bar >= MATRIX_BARS) return;
|
||||
if (matPlayhead >= 0) {
|
||||
for (let vi = 0; vi < MATRIX_VOICES.length; vi++) {
|
||||
const el = cellRefs[matPlayhead][vi];
|
||||
if (el) el.classList.remove("playing");
|
||||
}
|
||||
}
|
||||
matPlayhead = bar;
|
||||
for (let vi = 0; vi < MATRIX_VOICES.length; vi++) {
|
||||
const el = cellRefs[matPlayhead][vi];
|
||||
if (el) el.classList.add("playing");
|
||||
}
|
||||
updateTimelineHead(bar);
|
||||
});
|
||||
|
||||
on("/matrix/loop", (args) => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
on("/matrix/cell", (args) => {
|
||||
const vi = Math.round(Number(args[0]));
|
||||
const bar = Math.round(Number(args[1]));
|
||||
const color = Math.round(Number(args[2]));
|
||||
if (vi < 0 || vi >= MATRIX_VOICES.length || bar < 0 || bar >= MATRIX_BARS || color < 0 || color > 6) return;
|
||||
matGrid[vi][bar] = color;
|
||||
saveMatState();
|
||||
const el = cellRefs[bar][vi];
|
||||
if (el) {
|
||||
applyMatCellColor(el, color);
|
||||
if (bar === matPlayhead) el.classList.add("playing");
|
||||
}
|
||||
});
|
||||
|
||||
on("/matrix/grid", (args) => {
|
||||
if (!Array.isArray(args) || args.length < (MATRIX_VOICES.length * MATRIX_BARS)) return;
|
||||
for (let vi = 0; vi < MATRIX_VOICES.length; vi++) {
|
||||
for (let bar = 0; bar < MATRIX_BARS; bar++) {
|
||||
const v = Math.round(Number(args[vi * MATRIX_BARS + bar]));
|
||||
matGrid[vi][bar] = (Number.isFinite(v) && v >= 0 && v <= 6) ? v : 0;
|
||||
}
|
||||
}
|
||||
saveMatState();
|
||||
renderMatrix();
|
||||
});
|
||||
|
||||
on("/matrix/trig", (args) => {
|
||||
triggerGlow(Math.round(Number(args[0])), Number(args[1]));
|
||||
});
|
||||
|
||||
on("/matrix/instrument", (args) => {
|
||||
const vi = Math.round(Number(args[0])); const v = String(args[1] ?? "default");
|
||||
if (vi >= 0 && vi < MATRIX_VOICES.length) {
|
||||
matInst[vi] = v; saveMatState();
|
||||
const rowSel = document.querySelector(`.minst[data-vi="${vi}"]`);
|
||||
if (rowSel) rowSel.value = v;
|
||||
}
|
||||
});
|
||||
|
||||
on("/matrix/instruments", (args) => {
|
||||
for (let vi = 0; vi < MATRIX_VOICES.length && vi < args.length; vi++) {
|
||||
const v = String(args[vi] ?? "default"); matInst[vi] = v;
|
||||
const rowSel = document.querySelector(`.minst[data-vi="${vi}"]`);
|
||||
if (rowSel) rowSel.value = v;
|
||||
}
|
||||
saveMatState();
|
||||
});
|
||||
|
||||
export function init() {
|
||||
renderMatrix();
|
||||
renderTimeline();
|
||||
initTimelinePointer();
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import { send, on } from "./osc.js";
|
||||
import {
|
||||
PALETTE,
|
||||
MATRIX_VOICES, MATRIX_INST_CHOICES,
|
||||
MATRIX_MOD_SOURCES, MATRIX_MOD_TARGETS,
|
||||
matColorDefs, saveMatState,
|
||||
} from "./matrix-state.js";
|
||||
|
||||
let cdEditVoice = -1, cdColor = 1, cdAuditioning = false;
|
||||
|
||||
// Returns true when the modal is currently open for voice vi.
|
||||
export function isOpenFor(vi) {
|
||||
return cdEditVoice === vi && !document.getElementById("colordef-modal").hidden;
|
||||
}
|
||||
|
||||
// Public alias for openColorDef (Phase C will replace this signature).
|
||||
export function openFor(vi) {
|
||||
cdEditVoice = vi;
|
||||
cdColor = 1;
|
||||
document.getElementById("cd-title").textContent = MATRIX_VOICES[vi];
|
||||
document.getElementById("cd-audition").classList.toggle("on", cdAuditioning);
|
||||
renderColorSel(vi);
|
||||
renderPattern(vi);
|
||||
document.getElementById("colordef-modal").hidden = false;
|
||||
send("/matrix/colordefs/get", vi);
|
||||
send("/matrix/steps/get", vi);
|
||||
send("/matrix/colordefs/get/ext", vi);
|
||||
}
|
||||
|
||||
function closeColorDef() {
|
||||
if (cdAuditioning && cdEditVoice >= 0) { send("/matrix/audition", cdEditVoice, 0); cdAuditioning = false; }
|
||||
document.getElementById("cd-audition").classList.remove("on");
|
||||
document.getElementById("colordef-modal").hidden = true;
|
||||
cdEditVoice = -1;
|
||||
}
|
||||
|
||||
function setCD(color, field, value) {
|
||||
if (cdEditVoice < 0) return;
|
||||
matColorDefs[cdEditVoice][color][field] = value;
|
||||
saveMatState();
|
||||
send("/matrix/colordef", cdEditVoice, color, field, value);
|
||||
}
|
||||
|
||||
function renderColorSel(vi) {
|
||||
const sel = document.getElementById("cd-colorsel");
|
||||
if (!sel) return;
|
||||
sel.innerHTML = "";
|
||||
for (let c = 1; c <= 6; c++) {
|
||||
const btn = document.createElement("button");
|
||||
btn.textContent = String(c);
|
||||
btn.style.background = PALETTE[c-1];
|
||||
btn.style.borderColor = PALETTE[c-1];
|
||||
btn.classList.toggle("on", c === cdColor);
|
||||
btn.addEventListener("click", () => { cdColor = c; renderColorSel(vi); renderPattern(vi); });
|
||||
sel.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPattern(vi) {
|
||||
const p = document.getElementById("cd-pattern");
|
||||
if (!p) return;
|
||||
p.innerHTML = "";
|
||||
const c = cdColor;
|
||||
const d = matColorDefs[vi][c];
|
||||
if (!d) return;
|
||||
if (!d.steps) d.steps = new Array(16).fill(null);
|
||||
if (!d.pose) d.pose = [];
|
||||
if (d.mod === undefined) d.mod = null;
|
||||
|
||||
const targets = MATRIX_MOD_TARGETS[MATRIX_VOICES[vi]] || ["none","amp"];
|
||||
const hasCut = targets.includes("cutoff"), hasPan = targets.includes("pan");
|
||||
const mk = (label, el) => { const w = document.createElement("label");
|
||||
w.className = "cd-field"; w.textContent = label; w.appendChild(el); return w; };
|
||||
|
||||
// Instrument + variation row
|
||||
const varRow = document.createElement("div"); varRow.className = "cd-row m" + c;
|
||||
const inst = document.createElement("select");
|
||||
[["default","default"]].concat((MATRIX_INST_CHOICES[MATRIX_VOICES[vi]]||[]).map(s=>[s,s]))
|
||||
.forEach(([val,txt])=>{ const o=document.createElement("option"); o.value=val; o.textContent=txt; inst.appendChild(o); });
|
||||
inst.value = d.inst || "default";
|
||||
inst.addEventListener("change", () => setCD(c, "inst", inst.value));
|
||||
const stretch = document.createElement("select");
|
||||
[0.5,1.0,2.0].forEach(v=>{ const o=document.createElement("option"); o.value=v; o.textContent=v+"x"; stretch.appendChild(o); });
|
||||
stretch.value = d.stretch; stretch.addEventListener("change", () => setCD(c, "stretch", +stretch.value));
|
||||
const oct = document.createElement("select");
|
||||
[-1,0,1].forEach(v=>{ const o=document.createElement("option"); o.value=v; o.textContent=v; oct.appendChild(o); });
|
||||
oct.value = d.octave; oct.addEventListener("change", () => setCD(c, "octave", +oct.value));
|
||||
const amp = document.createElement("input"); amp.type="range"; amp.min=0; amp.max=1.5; amp.step=0.01;
|
||||
amp.value = d.amp; amp.addEventListener("change", () => setCD(c, "amp", +amp.value));
|
||||
varRow.appendChild(mk("inst", inst)); varRow.appendChild(mk("stretch", stretch));
|
||||
varRow.appendChild(mk("oct", oct)); varRow.appendChild(mk("amp", amp));
|
||||
if (hasCut) { const cut = document.createElement("input"); cut.type="range"; cut.min=200; cut.max=6000; cut.step=10;
|
||||
cut.value = d.cutoff > 0 ? d.cutoff : 1000;
|
||||
cut.addEventListener("change", () => setCD(c, "cutoff", +cut.value)); varRow.appendChild(mk("cutoff", cut)); }
|
||||
if (hasPan) { const pan = document.createElement("input"); pan.type="range"; pan.min=-1; pan.max=1; pan.step=0.01;
|
||||
pan.value = d.pan >= -1 ? d.pan : 0;
|
||||
pan.addEventListener("change", () => setCD(c, "pan", +pan.value)); varRow.appendChild(mk("pan", pan)); }
|
||||
p.appendChild(varRow);
|
||||
|
||||
// 16-step row
|
||||
const seqRow = document.createElement("div"); seqRow.className = "seq-row m" + c;
|
||||
for (let s = 0; s < 16; s++) {
|
||||
const cell = document.createElement("button"); cell.className = "seq-cell";
|
||||
const st = d.steps[s];
|
||||
cell.textContent = st ? st.degree : "";
|
||||
if (st) cell.classList.add("on");
|
||||
cell.addEventListener("click", () => {
|
||||
if (d.steps[s]) { d.steps[s] = null; send("/matrix/step", vi, c, s, -99, 0); }
|
||||
else { d.steps[s] = {degree:0, vel:0.8}; send("/matrix/step", vi, c, s, 0, 0.8); }
|
||||
saveMatState(); renderPattern(vi);
|
||||
});
|
||||
cell.addEventListener("wheel", (e) => {
|
||||
e.preventDefault(); const st2 = d.steps[s]; if (!st2) return;
|
||||
if (e.shiftKey) {
|
||||
st2.vel = Math.max(0, Math.min(1, st2.vel + (e.deltaY < 0 ? 0.1 : -0.1)));
|
||||
} else {
|
||||
st2.degree = Math.max(-48, Math.min(48, st2.degree + (e.deltaY < 0 ? 1 : -1)));
|
||||
}
|
||||
send("/matrix/step", vi, c, s, st2.degree, st2.vel); saveMatState(); renderPattern(vi);
|
||||
});
|
||||
seqRow.appendChild(cell);
|
||||
}
|
||||
p.appendChild(seqRow);
|
||||
|
||||
// Effect (mod) row
|
||||
const modDiv = document.createElement("div"); modDiv.className = "cd-row m" + c;
|
||||
const src = document.createElement("select");
|
||||
MATRIX_MOD_SOURCES.forEach(s=>{ const o=document.createElement("option"); o.value=s; o.textContent=s; src.appendChild(o); });
|
||||
const tgt = document.createElement("select");
|
||||
targets.forEach(t=>{ const o=document.createElement("option"); o.value=t; o.textContent=t; tgt.appendChild(o); });
|
||||
const dep = document.createElement("input"); dep.type="range"; dep.min=0; dep.max=1; dep.step=0.01;
|
||||
const mod = d.mod || {source:"none", target:"none", depth:0.5};
|
||||
src.value = mod.source; tgt.value = mod.target; dep.value = mod.depth;
|
||||
const sendMod = () => {
|
||||
matColorDefs[vi][c].mod = {source:src.value, target:tgt.value, depth:+dep.value};
|
||||
saveMatState(); send("/matrix/colormod", vi, c, src.value, tgt.value, +dep.value);
|
||||
};
|
||||
src.addEventListener("change", sendMod); tgt.addEventListener("change", sendMod); dep.addEventListener("change", sendMod);
|
||||
modDiv.appendChild(mk("src", src)); modDiv.appendChild(mk("tgt", tgt)); modDiv.appendChild(mk("depth", dep));
|
||||
p.appendChild(modDiv);
|
||||
|
||||
// Pose section
|
||||
const poseDiv = document.createElement("div");
|
||||
const addDiv = document.createElement("div");
|
||||
const poseId = document.createElement("input"); poseId.placeholder = "poseId"; poseId.className = "pose-id";
|
||||
const poseAct = document.createElement("select");
|
||||
["trigger","gate"].forEach(a=>{ const o=document.createElement("option"); o.value=a; o.textContent=a; poseAct.appendChild(o); });
|
||||
const poseGo = document.createElement("button"); poseGo.textContent = "+";
|
||||
poseGo.addEventListener("click", () => {
|
||||
if (!poseId.value) return;
|
||||
const poses = (d.pose || []).filter(b => b.poseId !== poseId.value);
|
||||
poses.push({poseId: poseId.value, action: poseAct.value});
|
||||
matColorDefs[vi][c].pose = poses;
|
||||
saveMatState(); send("/matrix/colorpose", vi, c, poses.length, ...poses.flatMap(b => [b.poseId, b.action]));
|
||||
renderPattern(vi);
|
||||
});
|
||||
[poseId, poseAct, poseGo].forEach(e => addDiv.appendChild(e));
|
||||
poseDiv.appendChild(addDiv);
|
||||
(d.pose || []).forEach(b => {
|
||||
const line = document.createElement("div");
|
||||
line.textContent = `${b.poseId} -> ${b.action}`;
|
||||
const del = document.createElement("button"); del.textContent = "x";
|
||||
del.style.cssText = "width:auto;padding:2px 6px;font-size:10px;";
|
||||
del.addEventListener("click", () => {
|
||||
matColorDefs[vi][c].pose = (d.pose || []).filter(pp => pp.poseId !== b.poseId);
|
||||
const poses = matColorDefs[vi][c].pose;
|
||||
saveMatState(); send("/matrix/colorpose", vi, c, poses.length, ...poses.flatMap(pp => [pp.poseId, pp.action]));
|
||||
renderPattern(vi);
|
||||
});
|
||||
line.appendChild(del); poseDiv.appendChild(line);
|
||||
});
|
||||
p.appendChild(poseDiv);
|
||||
}
|
||||
|
||||
// --- OSC handlers ---
|
||||
on("/matrix/colordefs", (args) => {
|
||||
const vi = Math.round(Number(args[0]));
|
||||
if (vi >= 0 && vi < MATRIX_VOICES.length) {
|
||||
for (let c = 1; c <= 6; c++) {
|
||||
const b = 1 + (c - 1) * 6;
|
||||
const ex = matColorDefs[vi][c] || {};
|
||||
matColorDefs[vi][c] = { stretch:Number(args[b]), octave:Math.round(Number(args[b+1])),
|
||||
amp:Number(args[b+2]), inst:String(args[b+3] ?? "default"),
|
||||
cutoff:Number(args[b+4]), pan:Number(args[b+5]),
|
||||
mod:ex.mod ?? null, pose:ex.pose || [], steps:ex.steps || new Array(16).fill(null) };
|
||||
}
|
||||
saveMatState();
|
||||
if (isOpenFor(vi)) { renderColorSel(vi); renderPattern(vi); }
|
||||
}
|
||||
});
|
||||
|
||||
on("/matrix/steps", (args) => {
|
||||
const vi = Math.round(Number(args[0])), c = Math.round(Number(args[1]));
|
||||
if (vi>=0 && vi<MATRIX_VOICES.length && c>=1 && c<=6 && matColorDefs[vi][c]) {
|
||||
if (!matColorDefs[vi][c].steps) matColorDefs[vi][c].steps = new Array(16).fill(null);
|
||||
for (let s=0; s<16; s++) {
|
||||
const deg = Number(args[2 + s*2]), vel = Number(args[3 + s*2]);
|
||||
matColorDefs[vi][c].steps[s] = (deg === -99) ? null : {degree:deg, vel:vel};
|
||||
}
|
||||
saveMatState();
|
||||
if (isOpenFor(vi) && cdColor === c) renderPattern(vi);
|
||||
}
|
||||
});
|
||||
|
||||
on("/matrix/colormod", (args) => {
|
||||
const vi=Math.round(Number(args[0])), c=Math.round(Number(args[1]));
|
||||
if (vi>=0&&vi<MATRIX_VOICES.length&&c>=1&&c<=6&&matColorDefs[vi][c]) {
|
||||
const src=String(args[2]??"none");
|
||||
matColorDefs[vi][c].mod = (src==="none") ? null : {source:src, target:String(args[3]??"none"), depth:Number(args[4])||0};
|
||||
saveMatState();
|
||||
if (isOpenFor(vi) && cdColor === c) renderPattern(vi);
|
||||
}
|
||||
});
|
||||
|
||||
on("/matrix/colorpose", (args) => {
|
||||
const vi=Math.round(Number(args[0])), c=Math.round(Number(args[1])), n=Math.round(Number(args[2]));
|
||||
if (vi>=0&&vi<MATRIX_VOICES.length&&c>=1&&c<=6&&matColorDefs[vi][c]) {
|
||||
matColorDefs[vi][c].pose = [];
|
||||
for (let k=0;k<n;k++){ const b=3+k*2; matColorDefs[vi][c].pose.push({poseId:String(args[b]),action:String(args[b+1])}); }
|
||||
saveMatState();
|
||||
if (isOpenFor(vi) && cdColor === c) renderPattern(vi);
|
||||
}
|
||||
});
|
||||
|
||||
export function init() {
|
||||
document.getElementById("cd-close").addEventListener("click", closeColorDef);
|
||||
document.getElementById("cd-audition").addEventListener("click", () => {
|
||||
if (cdEditVoice < 0) return;
|
||||
cdAuditioning = !cdAuditioning;
|
||||
send("/matrix/audition", cdEditVoice, cdAuditioning ? 1 : 0);
|
||||
document.getElementById("cd-audition").classList.toggle("on", cdAuditioning);
|
||||
});
|
||||
window.addEventListener("beforeunload", () => {
|
||||
if (cdAuditioning && cdEditVoice >= 0) send("/matrix/audition", cdEditVoice, 0);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user