customElements.define("webaudio-pianoroll", class Pianoroll extends HTMLElement { constructor() { super(); this.noteIdCounter = 0; this.editing = true this.refuse = false this.tool = 'create' } defineprop() { const plist = this.module.properties; for (let k in plist) { const v = plist[k]; this["_" + k] = this.getAttr(k, v.value); Object.defineProperty(this, k, { get: () => { return this["_" + k] }, set: (val) => { this["_" + k] = val; if (typeof (this[v.observer]) == "function") this[v.observer](); } }); } } marker(position, id, label) { const playhead = document.createElement("div"); playhead.className = "marker"; playhead.style.position = "absolute"; playhead.style.left = `${(position - this.xoffset) * this.stepw + this.yruler + this.kbwidth}px`; // Ajout de l'id et du label comme contenu de la div playhead.id = id; playhead.dataset.id = id; playhead.dataset.label = label; playhead.textContent = label; // Création du trait rouge const locator = document.createElement("div"); locator.style.position = "absolute"; locator.style.width = "2px"; // Épaisseur du trait locator.style.height = "100%"; // S'étend sur toute la hauteur du conteneur locator.style.backgroundColor = "red"; locator.style.left = '0px'; // Positionner au centre du playhead locator.style.top = '0px'; locator.style.transform = "translateX(-50%)"; // Centrer précisément le trait playhead.appendChild(locator); playhead.addEventListener('click', () => { console.log(`Playhead ID: ${id}`); }); playhead.addEventListener('mousedown', (e) => { const initialX = e.clientX; const initialLeft = parseInt(playhead.style.left, 10); const onMouseMove = (e) => { const deltaX = e.clientX - initialX; let newLeft = initialLeft + deltaX; let newPosition = (newLeft - this.yruler - this.kbwidth) / this.stepw + this.xoffset; newPosition = Math.round(newPosition / this.snap) * this.snap; newLeft = (newPosition - this.xoffset) * this.stepw + this.yruler + this.kbwidth; playhead.style.left = `${newLeft}px`; console.log(`Playhead ${id} moved to quantized position: ${newPosition}`); const markerEvent = this.sequence.find(ev => ev.id === id && ev.type === 'marker'); if (markerEvent) { markerEvent.t = newPosition; console.log(`Updated event in sequence for marker ID: ${id}, new position: ${newPosition}`); } }; const onMouseUp = () => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); this.canvas.parentElement.appendChild(playhead); const ev = { id: id, t: position, n: 0, // makers have no note info g: 0, // makers have no length f: 0, // not selected type: 'marker', details: {label: label, element: playhead}, textureApplied: false }; this.sequence.push(ev); } removeMarker(id) { const playhead = document.getElementById(id); if (playhead) { playhead.parentElement.removeChild(playhead); console.log(`Playhead with ID: ${id} has been removed.`); } else { console.log(`Playhead with ID: ${id} does not exist.`); } const eventIndex = this.sequence.findIndex(ev => ev.id === id && ev.type === 'marker'); if (eventIndex !== -1) { this.sequence.splice(eventIndex, 1); console.log(`Event associated with marker ID: ${id} has been removed from the sequence.`); } else { console.log(`No event found in the sequence for marker ID: ${id}.`); } } connectedCallback() { let root; root = this; this.module = { is: "webaudio-pianoroll", properties: { lowestnote: {type: Number, value: 33, observer: 'layout'}, highestnote: {type: Number, value: 39, observer: 'layout'}, width: {type: Number, value: 640, observer: 'layout'}, height: {type: Number, value: 320, observer: 'layout'}, timebase: {type: Number, value: 16, observer: 'layout'}, editmode: {type: String, value: "dragpoly"}, xrange: {type: Number, value: 16, observer: 'layout'}, yrange: {type: Number, value: 16, observer: 'layout'}, xoffset: {type: Number, value: 0, observer: 'layout'}, yoffset: {type: Number, value: 60, observer: 'layout'}, grid: {type: Number, value: 4}, snap: {type: Number, value: 1}, wheelzoom: {type: Number, value: 0}, wheelzoomx: {type: Number, value: 0}, wheelzoomy: {type: Number, value: 0}, xscroll: {type: Number, value: 0}, yscroll: {type: Number, value: 0}, gridnoteratio: {type: Number, value: 0.5, observer: 'updateTimer'}, xruler: {type: Number, value: 24, observer: 'layout'}, yruler: {type: Number, value: 24, observer: 'layout'}, octadj: {type: Number, value: -1}, cursor: {type: Number, value: 0, observer: 'redrawMarker'}, markstart: {type: Number, value: 0, observer: 'redrawMarker'}, markend: {type: Number, value: 8, observer: 'redrawMarker'}, defvelo: {type: Number, value: 100}, collt: {type: String, value: "#ccc"}, coldk: {type: String, value: "#aaa"}, colgrid: {type: String, value: "#666"}, colnote: {type: String, value: "#f22"}, colnotesel: {type: String, value: "#0f0"}, colnoteborder: {type: String, value: "#000"}, colnoteselborder: {type: String, value: "#fff"}, colrulerbg: {type: String, value: "#666"}, colrulerfg: {type: String, value: "#fff"}, colrulerborder: {type: String, value: "#000"}, colselarea: {type: String, value: "rgba(0,0,0,0.3)"}, bgsrc: {type: String, value: null, observer: 'layout'}, cursorsrc: { type: String, value: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJub25lIj4NCjxwYXRoIGZpbGw9InJnYmEoMjU1LDEwMCwxMDAsMC44KSIgZD0iTTAsMSAyNCwxMiAwLDIzIHoiLz4NCjwvc3ZnPg0K" }, cursoroffset: {type: Number, value: 0}, markstartsrc: { type: String, value: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4NCjxwYXRoIGZpbGw9IiMwYzAiIGQ9Ik0wLDEgMjQsMSAwLDIzIHoiLz4NCjwvc3ZnPg0K" }, markstartoffset: {type: Number, value: 0}, markendsrc: { type: String, value: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4NCjxwYXRoIGZpbGw9IiMwYzAiIGQ9Ik0wLDEgMjQsMSAyNCwyMyB6Ii8+DQo8L3N2Zz4NCg==" }, markendoffset: {type: Number, value: -24}, kbsrc: { type: String, value: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSI0ODAiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiPgo8cGF0aCBmaWxsPSIjZmZmIiBzdHJva2U9IiMwMDAiIGQ9Ik0wLDAgaDI0djQ4MGgtMjR6Ii8+CjxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik0wLDQwIGgxMnY0MGgtMTJ6IE0wLDEyMCBoMTJ2NDBoLTEyeiBNMCwyMDAgaDEydjQwaC0xMnogTTAsMzIwIGgxMnY0MGgtMTJ6IE0wLDQwMCBoMTJ2NDBoLTEyeiIvPgo8cGF0aCBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAiIGQ9Ik0wLDYwIGgyNCBNMCwxNDAgaDI0IE0wLDIyMCBoMjQgTTAsMjgwIGgyNCBNMCwzNDAgaDI0IE0wLDQyMCBoMjQiLz4KPC9zdmc+Cg==", observer: 'layout' }, kbwidth: {type: Number, value: 40}, loop: {type: Number, value: 0}, preload: {type: Number, value: 1.0}, tempo: {type: Number, value: 120, observer: 'updateTimer'}, enable: {type: Boolean, value: true}, }, }; this.defineprop(); root.innerHTML = `
Delete
`; this.sortSequence = function () { this.sequence.sort((x, y) => { return x.t - y.t; }); }; this.findNextEv = function (tick) { for (let i = 0; i < this.sequence.length; ++i) { const nev = this.sequence[i]; if (nev.t >= this.markend) return {t1: tick, n2: this.markend, dt: this.markend - tick, i: -1}; if (nev.t >= tick) return {t1: tick, t2: nev.t, dt: nev.t - tick, i: i}; } return {t1: tick, t2: this.markend, dt: this.markend - tick, i: -1}; }; this.locate = function (tick) { this.cursor = tick; }; this.updateTimer = function () { this.tick2time = 4 * 60 / this.tempo / this.timebase; }; this.play = function (playcallback, tick) { if (typeof (tick) != "undefined") { this.locate(tick); } if (this.timer != null) { return; } this.playcallback = playcallback; this.timestack = []; this.time0 = this.time1 = performance.now() / 1000 + 0.1; this.tick0 = this.tick1 = this.cursor; this.tick2time = 4 * 60 / this.tempo / this.timebase; const p = this.findNextEv(this.cursor); this.index1 = p.i; this.timestack.push([0, this.cursor, 0]); this.timestack.push([this.time0, this.cursor, this.tick2time]); this.time1 += p.dt * this.tick2time; if (p.i < 0) { this.timestack.push([this.time1, this.markstart, this.tick2time]); } else { this.timestack.push([this.time1, p.t1, this.tick2time]); } const frameRate = 1000 / 40; // 25ms, equivalent to 40 FPS let lastTime = performance.now(); const playLoop = () => { const currentTime = performance.now(); const deltaTime = currentTime - lastTime; if (deltaTime >= frameRate) { const current = performance.now() / 1000; while (this.timestack.length > 1 && current >= this.timestack[1][0]) { this.timestack.shift(); } this.cursor = this.timestack[0][1] + (current - this.timestack[0][0]) / this.timestack[0][2]; this.redrawMarker(); while (current + this.preload >= this.time1) { this.time0 = this.time1; this.tick0 = this.tick1; let e = this.sequence[this.index1]; if (!e || e.t >= this.markend) { this.timestack.push([this.time1, this.markstart, this.tick2time]); const p = this.findNextEv(this.markstart); this.time1 += p.dt * this.tick2time; this.index1 = p.i; } else { this.tick1 = e.t; this.timestack.push([this.time1, e.t, this.tick2time]); let gmax = Math.min(e.t + e.g, this.markend) - e.t; if (this.editmode == "gridmono" || this.editmode == "gridpoly") { gmax *= this.gridnoteratio; } const cbev = {t: this.time1, g: this.time1 + gmax * this.tick2time, n: e.n}; if (this.playcallback) { this.playcallback(cbev); } e = this.sequence[++this.index1]; if (!e || e.t >= this.markend) { this.time1 += (this.markend - this.tick1) * this.tick2time; const p = this.findNextEv(this.markstart); this.timestack.push([this.time1, this.markstart, this.tick2time]); this.time1 += p.dt * this.tick2time; this.index1 = p.i; } else { this.time1 += (e.t - this.tick1) * this.tick2time; } } } lastTime = currentTime; } this.timer = requestAnimationFrame(playLoop); }; this.timer = requestAnimationFrame(playLoop); this.stop = function () { if (this.timer) { cancelAnimationFrame(this.timer); this.timer = null; } }; }; this.stop = function () { if (this.timer) clearInterval(this.timer); this.timer = null; }; this.setMMLString = function (s) { this.sequence = []; let i, l, n, t, defo, defl, tie, evlast; const parse = {s: s, i: i, tb: this.timebase}; function getNum(p) { var n = 0; while (p.s[p.i] >= "0" && p.s[p.i] <= "9") { n = n * 10 + parseInt(p.s[p.i]); ++p.i; } return n; } function getLen(p) { var n = getNum(p); if (n == 0) n = defl; n = p.tb / n; var n2 = n; while (p.s[p.i] == ".") { ++p.i; n += (n2 >>= 1); } return n; } function getNote(p) { switch (p.s[p.i]) { case "c": case "C": n = 0; break; case "d": case "D": n = 2; break; case "e": case "E": n = 4; break; case "f": case "F": n = 5; break; case "g": case "G": n = 7; break; case "a": case "A": n = 9; break; case "b": case "B": n = 11; break; default: n = -1; } ++p.i; if (n < 0) return -1; for (; ;) { switch (p.s[p.i]) { case "-": --n; break; case "+": ++n; break; case "#": ++n; break; default: return n; } ++p.i; } } defo = 4; defl = 8; t = 0; tie = 0; evlast = null; for (parse.i = 0; parse.i < parse.s.length;) { switch (parse.s[parse.i]) { case '>': ++parse.i; ++defo; n = -1; l = 0; break; case '<': ++parse.i; --defo; n = -1; l = 0; break; case '&': case '^': ++parse.i; tie = 1; n = -1; l = 0; break; case 't': case 'T': ++parse.i; n = -1; l = 0; this.tempo = getNum(parse); break; case 'o': case 'O': ++parse.i; n = -1; l = 0; defo = getNum(parse); break; case 'l': case 'L': ++parse.i; n = -1; l = 0; defl = getNum(parse); break; case 'r': case 'R': ++parse.i; n = -1; l = getLen(parse); break; default: n = getNote(parse); if (n >= 0) l = getLen(parse); else l = 0; break; } if (n >= 0) { n = (defo - this.octadj) * 12 + n; if (tie && evlast && evlast.n == n) { evlast.g += l; tie = 0; } else this.sequence.push(evlast = {t: t, n: n, g: l, f: 0}); } t += l; } this.redraw(); }; this.getMMLString = function () { function makeNote(n, l, tb) { var mmlnote = ""; var ltab = [ [960, "1"], [840, "2.."], [720, "2."], [480, "2"], [420, "4.."], [360, "4."], [240, "4"], [210, "8.."], [180, "8."], [120, ""], [105, "16.."], [90, "16."], [60, "16"], [45, "32."], [30, "32"], [16, "60"], [15, "64"], [8, "120"], [4, "240"], [2, "480"], [1, "960"] ]; l = l * 960 / tb; while (l > 0) { for (let j = 0; j < ltab.length; ++j) { while (l >= ltab[j][0]) { l -= ltab[j][0]; mmlnote += "&" + n + ltab[j][1]; } } } return mmlnote.substring(1); } var mml = "t" + this.tempo + "o4l8"; var ti = 0, meas = 0, oct = 5, n; var notes = ["c", "d-", "d", "e-", "e", "f", "g-", "g", "a-", "a", "b-", "b"]; for (let i = 0; i < this.sequence.length; ++i) { var ev = this.sequence[i]; if (ev.t > ti) { var l = ev.t - ti; mml += makeNote("r", l, this.timebase); ti = ev.t; } var n = ev.n; if (n < oct * 12 || n >= oct * 12 + 12) { oct = (n / 12) | 0; mml += "o" + (oct + this.octadj); } n = notes[n % 12]; var l = ev.g; if (i + 1 < this.sequence.length) { var ev2 = this.sequence[i + 1]; if (ev2.t < ev.t + l) { l = ev2.t - ev.t; ti = ev2.t; } else ti = ev.t + ev.g; } else ti = ev.t + ev.g; mml += makeNote(n, l, this.timebase); } return mml; }; this.hitTest = function (pos) { const ht = {t: 0, n: 0, i: -1, m: " "}; const l = this.sequence.length; if (pos.t == this.menu) { ht.m = "m"; return ht; } ht.t = (this.xoffset + (pos.x - this.yruler - this.kbwidth) / this.swidth * this.xrange); ht.n = this.yoffset - (pos.y - this.height) / this.steph; if (pos.y >= this.height || pos.x >= this.width) { return ht; } if (pos.y < this.xruler) { ht.m = "x"; return ht; } if (pos.x < this.yruler + this.kbwidth) { ht.m = "y"; return ht; } for (let i = 0; i < l; ++i) { const ev = this.sequence[i]; if ((ht.n | 0) == ev.n) { if (ev.f && Math.abs(ev.t - ht.t) * this.stepw < 8) { ht.m = "B"; ht.i = i; return ht; } if (ev.f && Math.abs(ev.t + ev.g - ht.t) * this.stepw < 8) { ht.m = "E"; ht.i = i; return ht; } if (ht.t >= ev.t && ht.t < ev.t + ev.g) { ht.i = i; if (this.sequence[i].f) ht.m = "N"; else ht.m = "n"; return ht; } } } ht.m = "s"; return ht; }; this.applyTexture = function (ev) { if (this.noteTexture && this.noteTexture.complete) { const w = ev.g * this.stepw; const x = (ev.t - this.xoffset) * this.stepw + this.yruler + this.kbwidth; const y = this.height - (ev.n - this.yoffset) * this.steph; const x2 = (x + w) | 0; const y2 = (y - this.steph) | 0; if (ev.f) { this.ctx.fillStyle = this.colnotesel; // green if selected } else { this.ctx.fillStyle = this.colnote; // red if not selected } this.ctx.fillRect(x, y2, x2 - x, y - y2); this.ctx.globalAlpha = 0.5; // texture opacity this.ctx.drawImage(this.noteTexture, x, y2, x2 - x, y - y2); this.ctx.globalAlpha = 1.0; // réinitialiser l'opacité ev.textureApplied = true; } else { console.warn('noteTexture is not yet loaded or initialized.'); } }; this.addNote = function (t, n, g, v, f, type = 'note', details = {}) { if (t >= 0 && n >= 0 && n < 128) { const id = this.noteIdCounter++; const ev = {id: id, t: t, n: n, g: g, v: v, f: f, type: type, details: details}; console.log('programatic note creation : ' + id + ' type: ' + type + ', details' + details); if (!this.noteTexture || !this.noteTexture.complete) { this.noteTexture = new Image(); this.noteTexture.src = 'medias/images/waveform.png'; this.noteTexture.onload = () => { this.applyTexture(ev); }; } else { this.applyTexture(ev); } this.sequence.push(ev); this.sortSequence(); this.redraw(); return ev; } return null; }; this.selAreaNote = function (t1, t2, n1, n2) { let t, i = 0, e = this.sequence[i]; if (n1 > n2) t = n1, n1 = n2, n2 = t; if (t1 > t2) t = t1, t1 = t2, t2 = t; while (e) { if (e.t >= t1 && e.t < t2 && e.n >= n1 && e.n <= n2) e.f = 1; else e.f = 0; e = this.sequence[++i]; } }; this.delNote = function (idx) { this.sequence.splice(idx, 1); this.redraw(); }; this.delAreaNote = function (t, g, n) { const l = this.sequence.length; for (let i = l - 1; i >= 0; --i) { const ev = this.sequence[i]; if (typeof (n) != "undefined" && n != i) { if (t <= ev.t && t + g >= ev.t + ev.g) { this.sequence.splice(i, 1); } else if (t <= ev.t && t + g > ev.t && t + g < ev.t + ev.g) { ev.g = ev.t + ev.g - (t + g); ev.t = t + g; } else if (t >= ev.t && t < ev.t + ev.g && t + g >= ev.t + ev.g) { ev.g = t - ev.t; } else if (t > ev.t && t + g < ev.t + ev.g) { this.addNote(t + g, ev.n, ev.t + ev.g - t - g, this.defvelo); ev.g = t - ev.t; } } } }; this.delSelectedNote = function () { console.log('deleting note') const l = this.sequence.length; for (let i = l - 1; i >= 0; --i) { const ev = this.sequence[i]; if (ev.f) this.sequence.splice(i, 1); } this.refuse = true // to prevent any new note creation when clicking to delete }; this.moveSelectedNote = function (dt, dn) { console.log('moving note') const l = this.sequence.length; for (let i = 0; i < l; ++i) { const ev = this.sequence[i]; if (ev.f && ev.ot + dt < 0) dt = -ev.ot; } for (let i = 0; i < l; ++i) { const ev = this.sequence[i]; if (ev.f) { ev.t = (((ev.ot + dt) / this.snap + .5) | 0) * this.snap; ev.n = ev.on + dn; } } }; this.clearSel = function () { const l = this.sequence.length; for (let i = 0; i < l; ++i) { this.sequence[i].f = 0; } }; this.selectedNotes = function () { let obj = []; for (let i = this.sequence.length - 1; i >= 0; --i) { const ev = this.sequence[i]; if (ev.f) obj.push({i: i, ev: ev, t: ev.t, g: ev.g}); } return obj; }; this.editDragDown = function (pos) { const ht = this.hitTest(pos); let ev; if (ht.m == "N") { ev = this.sequence[ht.i]; this.dragging = {o: "D", m: "N", i: ht.i, t: ht.t, n: ev.n, dt: ht.t - ev.t}; for (let i = 0, l = this.sequence.length; i < l; ++i) { ev = this.sequence[i]; if (ev.f) ev.on = ev.n, ev.ot = ev.t, ev.og = ev.g; } this.redraw(); } else if (ht.m == "n") { ev = this.sequence[ht.i]; this.clearSel(); ev.f = 1; this.redraw(); } else if (ht.m == "E") { this.tool = 'trim_end' const ev = this.sequence[ht.i]; console.log('1 note end changed:'); this.dragging = {o: "D", m: "E", i: ht.i, t: ev.t, g: ev.g, ev: this.selectedNotes()}; } else if (ht.m == "B") { this.tool = 'trim_start' const ev = this.sequence[ht.i]; console.log('2 note start changed:'); this.dragging = {o: "D", m: "B", i: ht.i, t: ev.t, g: ev.g, ev: this.selectedNotes()}; } else if (ht.m == "s" && ht.t >= 0) { this.clearSel(); if (this.editing === true && !this.refuse) { var t = ((ht.t / this.snap) | 0) * this.snap; const id = this.noteIdCounter++; console.log('visual note creation : ' + id); var details = {in: 0, out: 0, group: {}}; const ev = { id: id, t: t, n: ht.n | 0, g: 1, f: 1, type: 'note', details: details, textureApplied: false }; this.sequence.push(ev); if (!this.noteTexture || !this.noteTexture.complete) { this.noteTexture = new Image(); this.noteTexture.src = 'medias/images/waveform.png'; this.noteTexture.onload = () => { this.applyTexture(ev); this.redraw(); }; } else { this.applyTexture(ev); this.redraw(); } this.dragging = { o: "D", m: "E", i: this.sequence.length - 1, t: t, g: 1, ev: [{t: t, g: 1, ev: this.sequence[this.sequence.length - 1]}] }; this.refuse = false; } else { switch (this.downht.m) { case "N": case "B": case "E": console.log('open menu'); this.popMenu(this.downpos); this.dragging = {o: "m"}; break; default: if (this.editmode == "dragmono" || this.editmode == "dragpoly") this.dragging = { o: "A", p: this.downpos, p2: this.downpos, t1: this.downht.t, n1: this.downht.n }; this.refuse = false console.log('===> accept/refuse' + this.refuse) break; } this.canvas.focus(); return false; } this.redraw(); } }; this.editDragMove = function (pos) { const ht = this.hitTest(pos); let ev, t; if (this.dragging.o == "D") { switch (this.dragging.m) { case "E": if (this.dragging.ev) { const dt = ((Math.max(0, ht.t) / this.snap + 0.9) | 0) * this.snap - this.dragging.t - this.dragging.g; const list = this.dragging.ev; for (let i = list.length - 1; i >= 0; --i) { const ev = list[i].ev; ev.g = list[i].g + dt; if (ev.g <= 0) ev.g = 1; if (this.editmove == "dragmono") this.delAreaNote(ev.t, ev.g); } } this.redraw(); break; case "B": if (this.dragging.ev) { const dt = ((Math.max(0, ht.t) / this.snap + 0.9) | 0) * this.snap - this.dragging.t; const list = this.dragging.ev; for (let i = list.length - 1; i >= 0; --i) { const ev = list[i].ev; ev.t = list[i].t + dt; ev.g = list[i].g - dt; if (ev.g <= 0) ev.g = 1; if (this.editmove == "dragmono") this.delAreaNote(ev.t, ev.g); } } this.redraw(); break; ev = this.sequence[this.dragging.i]; t = ((Math.max(0, ht.t) / this.snap + 0.5) | 0) * this.snap; ev.g = ev.t + ev.g - t; ev.t = t; if (ev.g < 0) { ev.t += ev.g; ev.g = -ev.g; this.dragging.m = "E"; } else if (ev.g == 0) { ev.t = t - 1; ev.g = 1; } this.redraw(); break; case "N": this.tool = 'drag' ev = this.sequence[this.dragging.i]; console.log('4 note. dragged : '); this.moveSelectedNote((ht.t - this.dragging.t) | 0, (ht.n | 0) - this.dragging.n); this.redraw(); break; } } }; this.editGridDown = function (pos) { const ht = this.hitTest(pos); if (ht.m == "n") { this.delNote(ht.i); this.dragging = {o: "G", m: "0"}; } else if (ht.m == "s" && ht.t >= 0) { const pt = Math.floor(ht.t); if (this.editmode == "gridmono") this.delAreaNote(pt, 1, ht.i); this.addNote(pt, ht.n | 0, 1, this.defvelo); this.dragging = {o: "G", m: "1"}; } }; this.editGridMove = function (pos) { const ht = this.hitTest(pos); if (this.dragging.o == "G") { switch (this.dragging.m) { case "1": const px = Math.floor(ht.t); if (ht.m == "s") { if (this.editmode == "gridmono") this.delAreaNote(px, 1, ht.i); this.addNote(px, ht.n | 0, 1, this.defvelo); } break; case "0": if (ht.m == "n") this.delNote(ht.i); break; } } }; this.setListener = function (el, mode) { this.bindcontextmenu = this.contextmenu.bind(this); this.bindpointermove = this.pointermove.bind(this); this.bindcancel = this.cancel.bind(this); el.addEventListener("mousedown", this.pointerdown.bind(this), true); el.addEventListener("touchstart", this.pointerdown.bind(this), false); if (mode) { el.addEventListener("mouseover", this.pointerover.bind(this), false); el.addEventListener("mouseout", this.pointerout.bind(this), false); } }; this.handleKeyboardClick = function (e) { const kbRect = this.kb.getBoundingClientRect(); const clickY = e.clientY - kbRect.top; const noteNumber = Math.floor(clickY / this.steph); console.log("note to trig :", noteNumber); }; this.ready = function () { this.body = root.children[1]; this.elem = root.childNodes[2]; this.proll = this.elem.children[0]; this.canvas = this.elem.children[0]; this.kb = this.elem.children[1]; this.ctx = this.canvas.getContext("2d"); this.kbimg = this.elem.children[1]; this.markstartimg = this.elem.children[2]; this.markendimg = this.elem.children[3]; this.cursorimg = this.elem.children[4]; this.menu = this.elem.children[5]; this.rcMenu = {x: 0, y: 0, width: 0, height: 0}; this.lastx = 0; this.lasty = 0; this.kb.addEventListener('click', this.handleKeyboardClick.bind(this), false); this.canvas.addEventListener('mousemove', this.mousemove.bind(this), false); this.canvas.addEventListener('keydown', this.keydown.bind(this), false); this.canvas.addEventListener('DOMMouseScroll', this.wheel.bind(this), false); this.canvas.addEventListener('mousewheel', this.wheel.bind(this), false); this.setListener(this.canvas, true); this.setListener(this.markendimg, true); this.setListener(this.markstartimg, true); this.setListener(this.cursorimg, true); this.setListener(this.menu, false); this.sequence = []; this.dragging = {o: null}; this.kbimg.style.height = this.sheight + "px"; this.kbimg.style.backgroundSize = (this.steph * 12) + "px"; this.layout(); this.initialized = 1; this.redraw(); }; this.setupImage = function () { }; this.preventScroll = function (e) { if (e.preventDefault) e.preventDefault(); }; this.getPos = function (e) { let t = null; if (e) { t = e.target; this.lastx = e.clientX - this.rcTarget.left; this.lasty = e.clientY - this.rcTarget.top; } if (this.lastx >= this.rcMenu.x && this.lastx < this.rcMenu.x + this.rcMenu.width && this.lasty >= this.rcMenu.y && this.lasty < this.rcMenu.y + this.rcMenu.height) t = this.menu; return {t: t, x: this.lastx, y: this.lasty}; }; this.contextmenu = function (e) { e.stopPropagation(); e.preventDefault(); window.removeEventListener("contextmenu", this.bindcontextmenu); return false; }; this.keydown = function (e) { switch (e.keyCode) { case 8: //delNote using backspace key this.delSelectedNote(); this.redraw(); break; } }; this.popMenu = function (pos) { console.log('pop menu call from shortcut') }; this.longtapcountup = function () { if (++this.longtapcount >= 18) { clearInterval(this.longtaptimer); switch (this.downht.m) { case "N": case "B": case "E": this.popMenu(this.downpos); this.dragging = {o: "m"}; break; } } }; this.pointerdown = function (ev) { let e; if (!this.enable) { console.log('here 1'); return; } if (ev.touches) { console.log('here 2'); e = ev.touches[0]; } else { e = ev; this.rcTarget = this.canvas.getBoundingClientRect(); this.downpos = this.getPos(e); this.downht = this.hitTest(this.downpos); if (this.downht.i >= 0) { let clickedNote = this.sequence[this.downht.i]; let noteId = clickedNote.id; console.log("Note ID :", noteId + ' note type: ' + clickedNote.type + ', note detail : ' + clickedNote.details); console.log('-- details below ---') console.log(clickedNote.details.group) console.log(clickedNote.details.in) console.log('-- end details ---') } this.longtapcount = 0; this.longtaptimer = setInterval(this.longtapcountup.bind(this), 100); window.addEventListener("touchmove", this.bindpointermove, false); window.addEventListener("mousemove", this.bindpointermove, false); window.addEventListener("touchend", this.bindcancel); window.addEventListener("mouseup", this.bindcancel); window.addEventListener("contextmenu", this.bindcontextmenu); if (e.button == 2 || e.ctrlKey) { console.log(' open the menu now!!!') switch (this.downht.m) { case "N": case "B": case "E": console.log('open menu'); this.popMenu(this.downpos); this.dragging = {o: "m"}; break; default: if (this.editmode == "dragmono" || this.editmode == "dragpoly") this.dragging = { o: "A", p: this.downpos, p2: this.downpos, t1: this.downht.t, n1: this.downht.n }; break; } ev.preventDefault(); ev.stopPropagation(); this.canvas.focus(); return false; } switch (e.target) { case this.markendimg: this.dragging = {o: "E", x: this.downpos.x, m: this.markend}; ev.preventDefault(); ev.stopPropagation(); return false; case this.markstartimg: this.dragging = {o: "S", x: this.downpos.x, m: this.markstart}; ev.preventDefault(); ev.stopPropagation(); return false; case this.cursorimg: this.dragging = {o: "P", x: this.downpos.x, m: this.cursor}; ev.preventDefault(); ev.stopPropagation(); return false; } this.dragging = { o: null, x: this.downpos.x, y: this.downpos.y, offsx: this.xoffset, offsy: this.yoffset }; this.canvas.focus(); switch (this.editmode) { case "gridpoly": case "gridmono": this.editGridDown(this.downpos); break; case "dragpoly": case "dragmono": this.editDragDown(this.downpos); break; } this.press = 1; if (ev.preventDefault) ev.preventDefault(); if (ev.stopPropagation) ev.stopPropagation(); return false; } }; this.mousemove = function (e) { if (this.dragging.o == null) { this.rcTarget = this.canvas.getBoundingClientRect(); const pos = this.getPos(e); const ht = this.hitTest(pos); switch (ht.m) { case "E": this.canvas.style.cursor = "e-resize"; break; case "B": this.canvas.style.cursor = "w-resize"; break; case "N": this.canvas.style.cursor = "move"; break; case "n": this.canvas.style.cursor = "pointer"; break; case "s": this.canvas.style.cursor = "pointer"; break; } } }; this.pointermove = function (ev) { let e; this.rcTarget = this.canvas.getBoundingClientRect(); if (ev.touches) e = ev.touches[0]; else e = ev; if (this.longtaptimer) clearInterval(this.longtaptimer); const pos = this.getPos(e); const ht = this.hitTest(pos); switch (this.dragging.o) { case null: if (this.xscroll) this.xoffset = this.dragging.offsx + (this.dragging.x - pos.x) * (this.xrange / this.width); if (this.yscroll) this.yoffset = this.dragging.offsy + (pos.y - this.dragging.y) * (this.yrange / this.height); break; case "m": if (ht.m == "m") { this.menu.style.background = "#ff6"; } else { this.menu.style.background = "#eef"; } break; case "A": this.dragging.p2 = pos; this.dragging.t2 = ht.t; this.dragging.n2 = ht.n; this.redraw(); break; case "E": console.log('marker end') var p = Math.max(1, (this.dragging.m + (pos.x - this.dragging.x) / this.stepw + .5) | 0); if (this.markstart >= p) this.markstart = p - 1; this.markend = p; break; case "S": console.log('marker start') var p = Math.max(0, (this.dragging.m + (pos.x - this.dragging.x) / this.stepw + .5) | 0); if (this.markend <= p) this.markend = p + 1; this.markstart = p; break; case "P": console.log('playhead') this.cursor = Math.max(0, (this.dragging.m + (pos.x - this.dragging.x) / this.stepw + .5) | 0); break; } switch (this.editmode) { case "gridpoly": case "gridmono": this.editGridMove(pos); break; case "dragpoly": case "dragmono": this.editDragMove(pos); break; } ev.preventDefault(); ev.stopPropagation(); return false; }; this.cancel = function (ev) { let e; if (ev.touches) e = null; else e = ev; if (this.longtaptimer) clearInterval(this.longtaptimer); const pos = this.getPos(e); if (this.dragging.o == "m") { } if (this.dragging.o == "A") { this.selAreaNote(this.dragging.t1, this.dragging.t2, this.dragging.n1, this.dragging.n2); this.dragging = {o: null}; this.redraw(); } if (this.editmode == "dragmono") { for (let ii = this.sequence.length - 1; ii >= 0; --ii) { const ev = this.sequence[ii]; if (ev && ev.f) { this.delAreaNote(ev.t, ev.g, ii); } } } this.redraw(); this.dragging = {o: null}; if (this.press) { this.sortSequence(); } this.press = 0; window.removeEventListener('touchstart', this.preventScroll, false); window.removeEventListener("mousemove", this.bindpointermove, false); window.removeEventListener("touchend", this.bindcancel, false); window.removeEventListener("mouseup", this.bindcancel, false); ev.preventDefault(); ev.stopPropagation(); return false; }; this.pointerover = function (e) { }; this.pointerout = function (e) { }; this.wheel = function (e) { let delta = 0; const pos = this.getPos(e); if (!e) e = window.event; if (e.wheelDelta) delta = e.wheelDelta / 120; else if (e.detail) delta = -e.detail / 3; const ht = this.hitTest(pos); if ((this.wheelzoomx || this.wheelzoom) && ht.m == "x") { if (delta > 0) { this.xoffset = ht.t - (ht.t - this.xoffset) / 1.2 this.xrange /= 1.2; } else { this.xoffset = ht.t - (ht.t - this.xoffset) * 1.2 this.xrange *= 1.2; } } if ((this.wheelzoomy || this.wheelzoom) && ht.m == "y") { if (delta > 0) { this.yoffset = ht.n - (ht.n - this.yoffset) / 1.2 this.yrange /= 1.2; } else { this.yoffset = ht.n - (ht.n - this.yoffset) * 1.2 this.yrange *= 1.2; } } e.preventDefault(); }; this.layout = function () { if (typeof (this.kbwidth) == "undefined") return; const proll = this.proll; const bodystyle = this.body.style; if (this.bgsrc) proll.style.background = "url('" + this.bgsrc + "')"; this.kbimg.style.background = "url('" + this.kbsrc + "')"; if (this.width) { proll.width = this.width; bodystyle.width = proll.style.width = this.width + "px"; } if (this.height) { proll.height = this.height; bodystyle.height = proll.style.height = this.height + "px"; } this.swidth = proll.width - this.yruler; this.swidth -= this.kbwidth; this.sheight = proll.height - this.xruler; this.redraw(); }; this.redrawMarker = function () { if (!this.initialized) return; const cur = (this.cursor - this.xoffset) * this.stepw + this.yruler + this.kbwidth; this.cursorimg.style.left = (cur + this.cursoroffset) + "px"; const start = (this.markstart - this.xoffset) * this.stepw + this.yruler + this.kbwidth; this.markstartimg.style.left = (start + this.markstartoffset) + "px"; const end = (this.markend - this.xoffset) * this.stepw + this.yruler + this.kbwidth; this.markendimg.style.left = (end + this.markendoffset) + "px"; }; this.redrawGrid = function () { for (let y = 0; y < 128; ++y) { if (this.semiflag[y % 12] & 1) this.ctx.fillStyle = this.coldk; else this.ctx.fillStyle = this.collt; let ys = this.height - (y - this.yoffset) * this.steph; this.ctx.fillRect(this.yruler + this.kbwidth, ys | 0, this.swidth, -this.steph); this.ctx.fillStyle = this.colgrid; this.ctx.fillRect(this.yruler + this.kbwidth, ys | 0, this.swidth, 1); } for (let t = 0; ; t += this.grid) { let x = this.stepw * (t - this.xoffset) + this.yruler + this.kbwidth; this.ctx.fillRect(x | 0, this.xruler, 1, this.sheight); if (x >= this.width) break; } }; this.semiflag = [6, 1, 0, 1, 0, 2, 1, 0, 1, 0, 1, 0]; this.redrawXRuler = function () { if (this.xruler) { this.ctx.textAlign = "left"; this.ctx.font = (this.xruler / 2) + "px 'sans-serif'"; this.ctx.fillStyle = this.colrulerbg; this.ctx.fillRect(0, 0, this.width, this.xruler); this.ctx.fillStyle = this.colrulerborder; this.ctx.fillRect(0, 0, this.width, 1); this.ctx.fillRect(0, 0, 1, this.xruler); this.ctx.fillRect(0, this.xruler - 1, this.width, 1); this.ctx.fillRect(this.width - 1, 0, 1, this.xruler); this.ctx.fillStyle = this.colrulerfg; for (let t = 0; ; t += this.timebase) { let x = (t - this.xoffset) * this.stepw + this.yruler + this.kbwidth; this.ctx.fillRect(x, 0, 1, this.xruler); this.ctx.fillText(t / this.timebase + 1, x + 4, this.xruler - 8); if (x >= this.width) break; } } }; this.redrawYRuler = function () { if (this.yruler) { this.ctx.textAlign = "right"; this.ctx.font = (this.steph / 2) + "px 'sans-serif'"; this.ctx.fillStyle = this.colrulerbg; this.ctx.fillRect(0, this.xruler, this.yruler, this.sheight); this.ctx.fillStyle = this.colrulerborder; this.ctx.fillRect(0, this.xruler, 1, this.sheight); this.ctx.fillRect(this.yruler, this.xruler, 1, this.sheight); this.ctx.fillRect(0, this.height - 1, this.yruler, 1); this.ctx.fillStyle = this.colrulerfg; for (let y = 0; y < 128; y += 12) { const ys = this.height - this.steph * (y - this.yoffset); this.ctx.fillRect(0, ys | 0, this.yruler, -1); this.ctx.fillText("C" + (((y / 12) | 0) + this.octadj), this.yruler - 4, ys - 4); } } this.kbimg.style.top = (this.xruler) + "px"; this.kbimg.style.left = this.yruler + "px"; this.kbimg.style.width = this.kbwidth + "px"; this.kbimg.style.backgroundSize = "100% " + (this.steph * 12) + "px"; this.kbimg.style.backgroundPosition = "0px " + (this.sheight + this.steph * this.yoffset) + "px"; }; this.redrawKeyboard = function () { if (this.yruler) { this.ctx.textAlign = "right"; this.ctx.font = (this.steph / 2) + "px 'sans-serif'"; this.ctx.fillStyle = this.colortab.kbwh; this.ctx.fillRect(1, this.xruler, this.yruler, this.sheight); this.ctx.fillStyle = this.colortab.kbbk; for (let y = 0; y < 128; ++y) { const ys = this.height - this.steph * (y - this.yoffset); const ysemi = y % 12; const fsemi = this.semiflag[ysemi]; if (fsemi & 1) { this.ctx.fillRect(0, ys, this.yruler / 2, -this.steph); this.ctx.fillRect(0, (ys - this.steph / 2) | 0, this.yruler, -1); } if (fsemi & 2) this.ctx.fillRect(0, ys | 0, this.yruler, -1); if (fsemi & 4) this.ctx.fillText("C" + (((y / 12) | 0) + this.octadj), this.yruler - 4, ys - 4); } this.ctx.fillRect(this.yruler, this.xruler, 1, this.sheight); } }; this.redrawAreaSel = function () { if (this.dragging && this.dragging.o == "A") { this.ctx.fillStyle = this.colselarea; this.ctx.fillRect(this.dragging.p.x, this.dragging.p.y, this.dragging.p2.x - this.dragging.p.x, this.dragging.p2.y - this.dragging.p.y); } }; this.redraw = function () { let x, w, y; if (!this.ctx) return; this.ctx.clearRect(0, 0, this.width, this.height); this.stepw = this.swidth / this.xrange; this.steph = this.sheight / this.yrange; this.redrawGrid(); const l = this.sequence.length; for (let s = 0; s < l; ++s) { const ev = this.sequence[s]; const noteHeight = this.steph; console.log('tool active: ' + this.tool + ', length : ' + ev.g + ' start: ' + ev.t) if (ev.f) { this.ctx.fillStyle = this.colnotesel; } else { this.ctx.fillStyle = this.colnote; } w = ev.g * this.stepw; x = (ev.t - this.xoffset) * this.stepw + this.yruler + this.kbwidth; y = this.height - (ev.n - this.yoffset) * this.steph; this.ctx.fillRect(x, y - noteHeight, w, noteHeight); this.applyTexture(ev); } this.redrawYRuler(); this.redrawXRuler(); this.redrawMarker(); this.redrawAreaSel(); }; this.ready(); } getAttr(n, def) { let v = this.getAttribute(n); if (v == "" || v == null) return def; switch (typeof (def)) { case "number": if (v == "true") return 1; v = +v; if (isNaN(v)) return 0; return v; } return v; } }); /// pianoroll creator : function aRoll(id, target, width, height) { // we build the pianoroll here const pianoRoll = document.createElement('webaudio-pianoroll'); pianoRoll.setAttribute('id', id); pianoRoll.setAttribute('width', width); pianoRoll.setAttribute('height', height); const targetElement = document.getElementById(target); if (targetElement) { targetElement.appendChild(pianoRoll); } else { console.error('Target element not found'); } } /// pianoroll builder below function setTempo(id) { let pianoRoll = document.getElementById(id); pianoRoll.tempo = 33; pianoRoll.updateTimer(); console.log('Tempo:', pianoRoll.tempo); } function changeEditMode(id, mode) { document.getElementById(id).editmode = mode; } function AddNote(id) { let sequence = document.getElementById(id); sequence.addNote( 0, // Tick 66, // Note 2, // Duration 39, // velocity??? 0, // selected or not 'notes' // type (group) ); } function setMarkStart(id) { let sequence = document.getElementById(id); sequence.markstart = (3) } function setMarkEnd(id) { let sequence = document.getElementById(id); sequence.markend = (7) } function playHead(id) { let sequence = document.getElementById(id); sequence.locate(3); } function menu(id) { console.log('open a menu here!!!') } function editing(id) { let sequence = document.getElementById(id); if (sequence.editing) { sequence.editing = false sequence.tool = 'select' console.log('no editing') } else { sequence.editing = true sequence.tool = 'create' console.log('editing active') } } function createExtendedNote(notes) { if (notes.length === 0) { return null; } let selectedNotes = notes.filter(note => note.f === 1); if (selectedNotes.length === 0) { return null; } let earliestStartNote = selectedNotes.reduce((earliest, note) => note.t < earliest.t ? note : earliest, selectedNotes[0]); let latestEndNote = selectedNotes.reduce((latest, note) => (note.t + note.g) > (latest.t + latest.g) ? note : latest, selectedNotes[0]); let newNote = { id: Math.max(...notes.map(note => note.id)) + 1, // new id build on previous build id t: earliestStartNote.t, // start of first selected note in timecode n: earliestStartNote.n, // use the same pitch as the first seledted note g: (latestEndNote.t + latestEndNote.g) - earliestStartNote.t, // compute total length f: 1 // by default select the newly created note }; return newNote; } function deleteSelectedNotes(id) { let pianoroll = document.getElementById(id); let sequence = pianoroll.sequence; if (Array.isArray(sequence)) { pianoroll.sequence = sequence.filter(note => note.f !== 1); if (typeof pianoroll.redraw === 'function') { pianoroll.redraw(); } } } function group(id) { let sequence = document.getElementById(id); let notes = sequence.sequence; let newNote = createExtendedNote(notes) let noteToDel = []; notes.forEach(note => { if (note.f === 1) { noteToDel.push(note) } }); noteToDel.forEach(note => { const index = sequence.sequence.indexOf(note); if (index !== -1) { sequence.sequence.splice(index, 1); } }); sequence.addNote(newNote.t, 60, newNote.g, 8, 1, 'group', {in: 0, out: 0, group: noteToDel}); } function notes(id) { let sequence = document.getElementById(id); let notes = sequence.sequence; console.log(notes) } function selectAll(id) { let pianoroll = document.getElementById(id); pianoroll.sequence.forEach(note => { note.f = 1; }); pianoroll.redraw(); } function deSelectAll(id) { let pianoroll = document.getElementById(id); pianoroll.sequence.forEach(note => { note.f = 0; }); pianoroll.redraw(); } function marker(id) { const pianoRoll = document.getElementById(id); pianoRoll.marker(12, 'playheadID1', 'My First Playhead'); } function removeMarker(id) { const pianoRoll = document.getElementById(id); pianoRoll.removeMarker('playheadID1'); } /// function clear_now() { console.clear() }