Merge pull request #29 from outis1one/claude/review-drum-game-nAeLJ
Add editor features: START marker, measure numbers, note duration, ra…
This commit is contained in:
+255
-47
@@ -209,7 +209,7 @@ canvas{display:block;}
|
|||||||
<span id="ed-status" style="color:#888;font-size:11px;margin-left:auto;"></span>
|
<span id="ed-status" style="color:#888;font-size:11px;margin-left:auto;"></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="ed-help" style="padding:4px 12px;background:#0d0d18;color:#666;font-size:11px;border-bottom:1px solid #222;">
|
<div id="ed-help" style="padding:4px 12px;background:#0d0d18;color:#666;font-size:11px;border-bottom:1px solid #222;">
|
||||||
Click = add note | Click note = select | Drag = move | Delete/Backspace = remove selected | Scroll = pan timeline
|
Click=add | Click note=select | Drag=move | Drag right edge=resize duration | D=add duration | Shift+D=remove duration | Shift+drag=select range | Ctrl+C/V=copy/paste | Ctrl+A=select all | Esc=deselect | Del=delete | Scroll=pan
|
||||||
<span id="ed-mode-label" style="margin-left:12px;color:#805ad5;"></span>
|
<span id="ed-mode-label" style="margin-left:12px;color:#805ad5;"></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="ed-code-panel" style="display:none;padding:8px 12px;background:#0a0a14;border-bottom:1px solid #333;max-height:200px;overflow:auto;">
|
<div id="ed-code-panel" style="display:none;padding:8px 12px;background:#0a0a14;border-bottom:1px solid #333;max-height:200px;overflow:auto;">
|
||||||
@@ -4793,7 +4793,9 @@ refreshMiniLB();
|
|||||||
|
|
||||||
// ── MELODY EDITOR ─────────────────────────────────────
|
// ── MELODY EDITOR ─────────────────────────────────────
|
||||||
var edPiece=null,edMelody=[],edBpm=120,edBars=8,edSelected=-1,edScrollX=0,edDragging=false,edDragNote=-1,edDragOffX=0,edDragOffY=0,edPreviewing=false,edIsClassical=false,edGenre="";
|
var edPiece=null,edMelody=[],edBpm=120,edBars=8,edSelected=-1,edScrollX=0,edDragging=false,edDragNote=-1,edDragOffX=0,edDragOffY=0,edPreviewing=false,edIsClassical=false,edGenre="";
|
||||||
var ED_LANE_H=50,ED_KICK_H=40,ED_HEAD=0,ED_NOTE_W=14,ED_NOTE_H=16;
|
var edResizing=false,edResizeNote=-1; // right-edge duration drag
|
||||||
|
var edSelRange=null,edSelecting=false,edClipboard=[]; // range select + copy/paste
|
||||||
|
var ED_LANE_H=50,ED_KICK_H=40,ED_HEAD=20,ED_NOTE_W=14,ED_NOTE_H=16,ED_MIN_DUR=0.05;
|
||||||
var ED_COLORS=["#e53e3e","#d69e2e","#3182ce","#38a169","#805ad5"];
|
var ED_COLORS=["#e53e3e","#d69e2e","#3182ce","#38a169","#805ad5"];
|
||||||
var ED_NAMES=["Red","Yellow","Blue","Green","Kick"];
|
var ED_NAMES=["Red","Yellow","Blue","Green","Kick"];
|
||||||
|
|
||||||
@@ -4830,19 +4832,18 @@ function openEditor(){
|
|||||||
// Load saved edits or use original
|
// Load saved edits or use original
|
||||||
var saved=edLoadSaved(edPiece.n);
|
var saved=edLoadSaved(edPiece.n);
|
||||||
if(saved){
|
if(saved){
|
||||||
edMelody=saved.melody.map(function(m){return{t:m.t,l:m.l};});
|
edMelody=saved.melody.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;});
|
||||||
edBpm=saved.bpm||edPiece.bpm;
|
edBpm=saved.bpm||edPiece.bpm;
|
||||||
if(!edIsClassical)edBars=saved.bars||edPiece.bars||8;
|
if(!edIsClassical)edBars=saved.bars||edPiece.bars||8;
|
||||||
}else if(edIsClassical){
|
}else if(edIsClassical){
|
||||||
edMelody=edPiece.melody.map(function(m){return{t:m.t,l:m.l};});
|
edMelody=edPiece.melody.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;});
|
||||||
edBpm=edPiece.bpm;
|
edBpm=edPiece.bpm;
|
||||||
}else{
|
}else{
|
||||||
// Regular rhythm: load the repeating pattern
|
edMelody=edPiece.p.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;});
|
||||||
edMelody=edPiece.p.map(function(m){return{t:m.t,l:m.l};});
|
|
||||||
edBpm=edPiece.bpm;
|
edBpm=edPiece.bpm;
|
||||||
edBars=edPiece.bars||8;
|
edBars=edPiece.bars||8;
|
||||||
}
|
}
|
||||||
edSelected=-1;edScrollX=0;
|
edSelected=-1;edScrollX=0;edSelRange=null;edClipboard=[];
|
||||||
document.getElementById("ed-title").textContent="Editing: "+edPiece.n+(edPiece.composer?" ("+edPiece.composer+")":"");
|
document.getElementById("ed-title").textContent="Editing: "+edPiece.n+(edPiece.composer?" ("+edPiece.composer+")":"");
|
||||||
document.getElementById("ed-bpm").value=edBpm;
|
document.getElementById("ed-bpm").value=edBpm;
|
||||||
document.getElementById("ed-mode-label").textContent=edIsClassical?"Classical (full timeline)":"Pattern (4-beat cycle, repeats "+edBars+"x)";
|
document.getElementById("ed-mode-label").textContent=edIsClassical?"Classical (full timeline)":"Pattern (4-beat cycle, repeats "+edBars+"x)";
|
||||||
@@ -4888,26 +4889,33 @@ function edDraw(){
|
|||||||
ctx.fillText("Kick",4,ED_HEAD+4*ED_LANE_H+15);
|
ctx.fillText("Kick",4,ED_HEAD+4*ED_LANE_H+15);
|
||||||
ctx.globalAlpha=1;
|
ctx.globalAlpha=1;
|
||||||
|
|
||||||
// Grid lines
|
// Measure header background
|
||||||
|
ctx.fillStyle="#0a0a12";
|
||||||
|
ctx.fillRect(0,0,W,ED_HEAD);
|
||||||
|
|
||||||
|
// Grid lines + measure numbers
|
||||||
if(edIsClassical){
|
if(edIsClassical){
|
||||||
// Classical: grid by beats (seconds / (60/bpm))
|
|
||||||
var beatSec=60/edBpm;
|
var beatSec=60/edBpm;
|
||||||
var startBeat=Math.floor(edXToTime(0)/beatSec);
|
var startBeat=Math.floor(edXToTime(0)/beatSec);
|
||||||
var endBeat=Math.ceil(edXToTime(W)/beatSec);
|
var endBeat=Math.ceil(edXToTime(W)/beatSec);
|
||||||
for(var b=startBeat;b<=endBeat;b++){
|
for(var b=startBeat;b<=endBeat;b++){
|
||||||
var bx=edTimeToX(b*beatSec);
|
var bx=edTimeToX(b*beatSec);
|
||||||
if(bx<0||bx>W)continue;
|
if(bx<0||bx>W)continue;
|
||||||
ctx.strokeStyle=b%4===0?"#444":"#222";
|
var isMeasure=b%4===0;
|
||||||
ctx.lineWidth=b%4===0?1.5:0.5;
|
ctx.strokeStyle=isMeasure?"#444":"#222";
|
||||||
|
ctx.lineWidth=isMeasure?1.5:0.5;
|
||||||
ctx.beginPath();ctx.moveTo(bx,ED_HEAD);ctx.lineTo(bx,H);ctx.stroke();
|
ctx.beginPath();ctx.moveTo(bx,ED_HEAD);ctx.lineTo(bx,H);ctx.stroke();
|
||||||
if(b%4===0){
|
if(isMeasure){
|
||||||
|
// Measure number in header
|
||||||
|
var mNum=Math.floor(b/4)+1;
|
||||||
|
ctx.fillStyle="#888";ctx.font="bold 11px sans-serif";
|
||||||
|
ctx.fillText("M"+mNum,bx+3,ED_HEAD-5);
|
||||||
|
// Time at bottom
|
||||||
ctx.fillStyle="#555";ctx.font="10px sans-serif";
|
ctx.fillStyle="#555";ctx.font="10px sans-serif";
|
||||||
ctx.fillText((b*beatSec).toFixed(1)+"s",bx+2,H-4);
|
ctx.fillText((b*beatSec).toFixed(1)+"s",bx+2,H-4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
// Regular rhythm: grid by beat subdivisions (t=0 to 4)
|
|
||||||
// Draw 16th note grid (every 0.25 beats)
|
|
||||||
for(var sub=0;sub<=16;sub++){
|
for(var sub=0;sub<=16;sub++){
|
||||||
var bt=sub*0.25;
|
var bt=sub*0.25;
|
||||||
var bx=edTimeToX(bt);
|
var bx=edTimeToX(bt);
|
||||||
@@ -4919,11 +4927,22 @@ function edDraw(){
|
|||||||
ctx.beginPath();ctx.moveTo(bx,ED_HEAD);ctx.lineTo(bx,H);ctx.stroke();
|
ctx.beginPath();ctx.moveTo(bx,ED_HEAD);ctx.lineTo(bx,H);ctx.stroke();
|
||||||
if(isBeat){
|
if(isBeat){
|
||||||
ctx.fillStyle="#888";ctx.font="bold 11px sans-serif";
|
ctx.fillStyle="#888";ctx.font="bold 11px sans-serif";
|
||||||
|
ctx.fillText("Beat "+(sub/4+1),bx+3,ED_HEAD-5);
|
||||||
ctx.fillText("Beat "+(sub/4+1),bx+3,H-4);
|
ctx.fillText("Beat "+(sub/4+1),bx+3,H-4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// START marker at t=0
|
||||||
|
var sx=edTimeToX(0);
|
||||||
|
if(sx>-2&&sx<W){
|
||||||
|
ctx.strokeStyle="#38a169";ctx.lineWidth=2;ctx.setLineDash([4,4]);
|
||||||
|
ctx.beginPath();ctx.moveTo(sx,0);ctx.lineTo(sx,H);ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.fillStyle="#38a169";ctx.font="bold 10px sans-serif";
|
||||||
|
ctx.fillText("START",sx+3,12);
|
||||||
|
}
|
||||||
|
|
||||||
// Lane separator lines
|
// Lane separator lines
|
||||||
for(i=1;i<5;i++){
|
for(i=1;i<5;i++){
|
||||||
var ly=ED_HEAD+i*ED_LANE_H;
|
var ly=ED_HEAD+i*ED_LANE_H;
|
||||||
@@ -4931,17 +4950,40 @@ function edDraw(){
|
|||||||
ctx.beginPath();ctx.moveTo(0,ly);ctx.lineTo(W,ly);ctx.stroke();
|
ctx.beginPath();ctx.moveTo(0,ly);ctx.lineTo(W,ly);ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw notes
|
// Draw selection range highlight
|
||||||
|
if(edSelRange){
|
||||||
|
var rx1=edTimeToX(Math.min(edSelRange.start,edSelRange.end));
|
||||||
|
var rx2=edTimeToX(Math.max(edSelRange.start,edSelRange.end));
|
||||||
|
ctx.fillStyle="rgba(128,90,213,0.15)";
|
||||||
|
ctx.fillRect(rx1,ED_HEAD,rx2-rx1,H-ED_HEAD);
|
||||||
|
ctx.strokeStyle="#805ad5";ctx.lineWidth=1;ctx.setLineDash([3,3]);
|
||||||
|
ctx.beginPath();ctx.moveTo(rx1,ED_HEAD);ctx.lineTo(rx1,H);ctx.stroke();
|
||||||
|
ctx.beginPath();ctx.moveTo(rx2,ED_HEAD);ctx.lineTo(rx2,H);ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw notes (with duration support)
|
||||||
edMelody.forEach(function(m,idx){
|
edMelody.forEach(function(m,idx){
|
||||||
var nx=edTimeToX(m.t)-ED_NOTE_W/2;
|
var dur=m.d||0;
|
||||||
|
var noteW=dur>0?Math.max(ED_NOTE_W, dur*pps):ED_NOTE_W;
|
||||||
|
var nx=dur>0?edTimeToX(m.t):edTimeToX(m.t)-ED_NOTE_W/2;
|
||||||
var ny=edLaneToY(m.l)-ED_NOTE_H/2;
|
var ny=edLaneToY(m.l)-ED_NOTE_H/2;
|
||||||
if(nx+ED_NOTE_W<0||nx>W)return;
|
if(nx+noteW<0||nx>W)return;
|
||||||
|
var inRange=edSelRange&&!edSelecting&&m.t>=Math.min(edSelRange.start,edSelRange.end)&&m.t<=Math.max(edSelRange.start,edSelRange.end);
|
||||||
ctx.fillStyle=ED_COLORS[m.l]||"#888";
|
ctx.fillStyle=ED_COLORS[m.l]||"#888";
|
||||||
ctx.globalAlpha=idx===edSelected?1:0.8;
|
ctx.globalAlpha=idx===edSelected?1:inRange?0.95:0.8;
|
||||||
ctx.fillRect(nx,ny,ED_NOTE_W,ED_NOTE_H);
|
ctx.fillRect(nx,ny,noteW,ED_NOTE_H);
|
||||||
|
// Duration right-edge handle
|
||||||
|
if(dur>0){
|
||||||
|
ctx.fillStyle="rgba(255,255,255,0.3)";
|
||||||
|
ctx.fillRect(nx+noteW-4,ny,4,ED_NOTE_H);
|
||||||
|
}
|
||||||
if(idx===edSelected){
|
if(idx===edSelected){
|
||||||
ctx.strokeStyle="#fff";ctx.lineWidth=2;
|
ctx.strokeStyle="#fff";ctx.lineWidth=2;
|
||||||
ctx.strokeRect(nx-1,ny-1,ED_NOTE_W+2,ED_NOTE_H+2);
|
ctx.strokeRect(nx-1,ny-1,noteW+2,ED_NOTE_H+2);
|
||||||
|
} else if(inRange){
|
||||||
|
ctx.strokeStyle="#805ad5";ctx.lineWidth=1.5;
|
||||||
|
ctx.strokeRect(nx-1,ny-1,noteW+2,ED_NOTE_H+2);
|
||||||
}
|
}
|
||||||
ctx.globalAlpha=1;
|
ctx.globalAlpha=1;
|
||||||
});
|
});
|
||||||
@@ -4960,41 +5002,82 @@ function edDraw(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
document.getElementById("ed-status").textContent=edMelody.length+" notes | "+
|
var statusTxt=edMelody.length+" notes";
|
||||||
(edSelected>=0?"Selected #"+edSelected+" (t="+edMelody[edSelected].t.toFixed(3)+"s, "+ED_NAMES[edMelody[edSelected].l]+")":"Click to add, click note to select");
|
if(edSelRange&&!edSelecting){
|
||||||
|
var lo=Math.min(edSelRange.start,edSelRange.end),hi=Math.max(edSelRange.start,edSelRange.end);
|
||||||
|
var cnt=edMelody.filter(function(m){return m.t>=lo&&m.t<=hi;}).length;
|
||||||
|
statusTxt+=" | Range: "+lo.toFixed(2)+"s–"+hi.toFixed(2)+"s ("+cnt+" notes) | Ctrl+C copy, Ctrl+V paste, Del delete";
|
||||||
|
}else if(edSelected>=0){
|
||||||
|
var sm=edMelody[edSelected];
|
||||||
|
statusTxt+=" | Selected #"+edSelected+" (t="+sm.t.toFixed(3)+"s, "+ED_NAMES[sm.l];
|
||||||
|
if(sm.d>0)statusTxt+=", dur="+sm.d.toFixed(3)+"s";
|
||||||
|
statusTxt+=") | D=add duration, Shift+D=remove";
|
||||||
|
}else{
|
||||||
|
statusTxt+=" | Click=add, Shift+drag=select range";
|
||||||
|
}
|
||||||
|
document.getElementById("ed-status").textContent=statusTxt;
|
||||||
}
|
}
|
||||||
|
|
||||||
function edHitTest(x,y){
|
function edHitTest(x,y){
|
||||||
|
var pps=edPxPerUnit();
|
||||||
for(var i=edMelody.length-1;i>=0;i--){
|
for(var i=edMelody.length-1;i>=0;i--){
|
||||||
var m=edMelody[i];
|
var m=edMelody[i];
|
||||||
var nx=edTimeToX(m.t)-ED_NOTE_W/2;
|
var dur=m.d||0;
|
||||||
|
var noteW=dur>0?Math.max(ED_NOTE_W,dur*pps):ED_NOTE_W;
|
||||||
|
var nx=dur>0?edTimeToX(m.t):edTimeToX(m.t)-ED_NOTE_W/2;
|
||||||
var ny=edLaneToY(m.l)-ED_NOTE_H/2;
|
var ny=edLaneToY(m.l)-ED_NOTE_H/2;
|
||||||
if(x>=nx&&x<=nx+ED_NOTE_W&&y>=ny&&y<=ny+ED_NOTE_H)return i;
|
if(x>=nx&&x<=nx+noteW&&y>=ny&&y<=ny+ED_NOTE_H)return i;
|
||||||
}
|
}
|
||||||
return-1;
|
return-1;
|
||||||
}
|
}
|
||||||
|
function edIsOnRightEdge(x,idx){
|
||||||
|
var m=edMelody[idx],pps=edPxPerUnit();
|
||||||
|
var dur=m.d||0;
|
||||||
|
if(dur<=0)return false;
|
||||||
|
var noteW=Math.max(ED_NOTE_W,dur*pps);
|
||||||
|
var nx=edTimeToX(m.t);
|
||||||
|
var rightX=nx+noteW;
|
||||||
|
return Math.abs(x-rightX)<8;
|
||||||
|
}
|
||||||
|
|
||||||
// Mouse handlers
|
// Mouse handlers
|
||||||
function edMouseDown(e){
|
function edMouseDown(e){
|
||||||
var cv=getEdCanvas(),rect=cv.getBoundingClientRect();
|
var cv=getEdCanvas(),rect=cv.getBoundingClientRect();
|
||||||
var mx=e.clientX-rect.left,my=e.clientY-rect.top;
|
var mx=e.clientX-rect.left,my=e.clientY-rect.top;
|
||||||
|
|
||||||
|
// Shift+click = range select
|
||||||
|
if(e.shiftKey){
|
||||||
|
var t=edXToTime(mx);
|
||||||
|
edSelRange={start:t,end:t};
|
||||||
|
edSelecting=true;
|
||||||
|
edSelected=-1;
|
||||||
|
edDraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var hit=edHitTest(mx,my);
|
var hit=edHitTest(mx,my);
|
||||||
if(hit>=0){
|
if(hit>=0){
|
||||||
|
// Check if clicking right edge for resize
|
||||||
|
if(edIsOnRightEdge(mx,hit)){
|
||||||
|
edResizing=true;edResizeNote=hit;edSelected=hit;
|
||||||
|
edDraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
edSelected=hit;
|
edSelected=hit;
|
||||||
edDragging=true;edDragNote=hit;
|
edDragging=true;edDragNote=hit;
|
||||||
edDragOffX=mx-edTimeToX(edMelody[hit].t);
|
edDragOffX=mx-edTimeToX(edMelody[hit].t);
|
||||||
edDragOffY=my-edLaneToY(edMelody[hit].l);
|
edDragOffY=my-edLaneToY(edMelody[hit].l);
|
||||||
|
edSelRange=null; // clear range on note click
|
||||||
edDraw();
|
edDraw();
|
||||||
}else{
|
}else{
|
||||||
// Add new note
|
// Click on empty space clears range and adds note
|
||||||
|
edSelRange=null;
|
||||||
var t=edXToTime(mx);
|
var t=edXToTime(mx);
|
||||||
var l=edYToLane(my);
|
var l=edYToLane(my);
|
||||||
if(l>=0&&l<=4&&t>=0){
|
if(l>=0&&l<=4&&t>=0){
|
||||||
// Snap to nearest subdivision
|
var snap=edIsClassical?(60/edBpm/4):0.25;
|
||||||
var snap=edIsClassical?(60/edBpm/4):0.25; // 1/4 beat in seconds or 1/16 note in beat units
|
|
||||||
t=Math.round(t/snap)*snap;
|
t=Math.round(t/snap)*snap;
|
||||||
t=parseFloat(t.toFixed(4));
|
t=parseFloat(t.toFixed(4));
|
||||||
// For regular rhythms, clamp to 0-4 range
|
|
||||||
if(!edIsClassical)t=Math.max(0,Math.min(3.99,t));
|
if(!edIsClassical)t=Math.max(0,Math.min(3.99,t));
|
||||||
edMelody.push({t:t,l:l});
|
edMelody.push({t:t,l:l});
|
||||||
edMelody.sort(function(a,b){return a.t-b.t;});
|
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||||
@@ -5004,25 +5087,62 @@ function edMouseDown(e){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function edMouseMove(e){
|
function edMouseMove(e){
|
||||||
if(!edDragging||edDragNote<0)return;
|
|
||||||
var cv=getEdCanvas(),rect=cv.getBoundingClientRect();
|
var cv=getEdCanvas(),rect=cv.getBoundingClientRect();
|
||||||
var mx=e.clientX-rect.left,my=e.clientY-rect.top;
|
var mx=e.clientX-rect.left,my=e.clientY-rect.top;
|
||||||
|
|
||||||
|
// Range selecting
|
||||||
|
if(edSelecting&&edSelRange){
|
||||||
|
edSelRange.end=edXToTime(mx);
|
||||||
|
edDraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resizing note duration
|
||||||
|
if(edResizing&&edResizeNote>=0){
|
||||||
|
var m=edMelody[edResizeNote];
|
||||||
|
var snap=edIsClassical?(60/edBpm/4):0.25;
|
||||||
|
var newEnd=edXToTime(mx);
|
||||||
|
var dur=Math.max(ED_MIN_DUR, newEnd-m.t);
|
||||||
|
dur=Math.round(dur/snap)*snap;
|
||||||
|
dur=Math.max(snap, dur);
|
||||||
|
m.d=parseFloat(dur.toFixed(4));
|
||||||
|
edDraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dragging note position
|
||||||
|
if(!edDragging||edDragNote<0)return;
|
||||||
var t=edXToTime(mx-edDragOffX);
|
var t=edXToTime(mx-edDragOffX);
|
||||||
var l=edYToLane(my);
|
var l=edYToLane(my);
|
||||||
var snap=60/edBpm/4;
|
var snap2=60/edBpm/4;
|
||||||
t=Math.max(0,Math.round(t/snap)*snap);
|
t=Math.max(0,Math.round(t/snap2)*snap2);
|
||||||
if(l>=0&&l<=4){
|
if(l>=0&&l<=4){
|
||||||
edMelody[edDragNote].t=t;
|
edMelody[edDragNote].t=t;
|
||||||
edMelody[edDragNote].l=l;
|
edMelody[edDragNote].l=l;
|
||||||
edSelected=edDragNote;
|
edSelected=edDragNote;
|
||||||
edDraw();
|
edDraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update cursor for right-edge hover
|
||||||
|
var hit2=edHitTest(mx,my);
|
||||||
|
cv.style.cursor=(hit2>=0&&edIsOnRightEdge(mx,hit2))?"ew-resize":(hit2>=0?"grab":"crosshair");
|
||||||
}
|
}
|
||||||
function edMouseUp(){
|
function edMouseUp(){
|
||||||
|
if(edSelecting){
|
||||||
|
edSelecting=false;
|
||||||
|
// If range is too small, clear it
|
||||||
|
if(edSelRange&&Math.abs(edSelRange.end-edSelRange.start)<0.01)edSelRange=null;
|
||||||
|
edDraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(edResizing){
|
||||||
|
edResizing=false;edResizeNote=-1;
|
||||||
|
edDraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(edDragging){
|
if(edDragging){
|
||||||
edDragging=false;edDragNote=-1;
|
edDragging=false;edDragNote=-1;
|
||||||
edMelody.sort(function(a,b){return a.t-b.t;});
|
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||||
// Re-find selected after sort
|
|
||||||
edDraw();
|
edDraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5034,12 +5154,81 @@ function edWheel(e){
|
|||||||
}
|
}
|
||||||
function edKeyDown(e){
|
function edKeyDown(e){
|
||||||
if(!document.getElementById("sed").classList.contains("active"))return;
|
if(!document.getElementById("sed").classList.contains("active"))return;
|
||||||
|
|
||||||
|
// Ctrl+C — copy selected range or selected note
|
||||||
|
if((e.ctrlKey||e.metaKey)&&e.key==="c"){
|
||||||
|
if(edSelRange){
|
||||||
|
var lo=Math.min(edSelRange.start,edSelRange.end);
|
||||||
|
var hi=Math.max(edSelRange.start,edSelRange.end);
|
||||||
|
edClipboard=edMelody.filter(function(m){return m.t>=lo&&m.t<=hi;})
|
||||||
|
.map(function(m){return{t:m.t-lo,l:m.l,d:m.d||0};});
|
||||||
|
document.getElementById("ed-status").textContent="Copied "+edClipboard.length+" notes";
|
||||||
|
}else if(edSelected>=0){
|
||||||
|
var m=edMelody[edSelected];
|
||||||
|
edClipboard=[{t:0,l:m.l,d:m.d||0}];
|
||||||
|
document.getElementById("ed-status").textContent="Copied 1 note";
|
||||||
|
}
|
||||||
|
e.preventDefault();return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+V — paste at end of current notes or at selection start
|
||||||
|
if((e.ctrlKey||e.metaKey)&&e.key==="v"){
|
||||||
|
if(edClipboard.length===0)return;
|
||||||
|
// Paste after the last note, or at end of selection range
|
||||||
|
var pasteAt;
|
||||||
|
if(edSelRange){
|
||||||
|
pasteAt=Math.max(edSelRange.start,edSelRange.end);
|
||||||
|
}else{
|
||||||
|
var maxT=0;
|
||||||
|
edMelody.forEach(function(m){var end=m.t+(m.d||0);if(end>maxT)maxT=end;if(m.t>maxT)maxT=m.t;});
|
||||||
|
var snap=edIsClassical?(60/edBpm/4):0.25;
|
||||||
|
pasteAt=Math.ceil(maxT/snap)*snap; // snap to next grid line
|
||||||
|
}
|
||||||
|
edClipboard.forEach(function(c){
|
||||||
|
var note={t:parseFloat((pasteAt+c.t).toFixed(4)),l:c.l};
|
||||||
|
if(c.d>0)note.d=c.d;
|
||||||
|
edMelody.push(note);
|
||||||
|
});
|
||||||
|
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||||
|
edSelected=-1;edSelRange=null;
|
||||||
|
edDraw();
|
||||||
|
document.getElementById("ed-status").textContent="Pasted "+edClipboard.length+" notes at t="+pasteAt.toFixed(2);
|
||||||
|
e.preventDefault();return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+A — select all (set range over all notes)
|
||||||
|
if((e.ctrlKey||e.metaKey)&&e.key==="a"&&edMelody.length>0){
|
||||||
|
var first=edMelody[0].t;
|
||||||
|
var last=edMelody[edMelody.length-1].t+(edMelody[edMelody.length-1].d||0);
|
||||||
|
edSelRange={start:first,end:last};
|
||||||
|
edSelected=-1;
|
||||||
|
edDraw();
|
||||||
|
e.preventDefault();return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape — clear selection range
|
||||||
|
if(e.key==="Escape"){
|
||||||
|
edSelRange=null;edSelected=-1;edDraw();e.preventDefault();return;
|
||||||
|
}
|
||||||
|
|
||||||
if((e.key==="Delete"||e.key==="Backspace")&&edSelected>=0){
|
if((e.key==="Delete"||e.key==="Backspace")&&edSelected>=0){
|
||||||
edMelody.splice(edSelected,1);
|
edMelody.splice(edSelected,1);
|
||||||
edSelected=-1;
|
edSelected=-1;
|
||||||
edDraw();
|
edDraw();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
// Delete selected range
|
||||||
|
if((e.key==="Delete"||e.key==="Backspace")&&edSelRange&&edSelected<0){
|
||||||
|
var lo2=Math.min(edSelRange.start,edSelRange.end);
|
||||||
|
var hi2=Math.max(edSelRange.start,edSelRange.end);
|
||||||
|
var before=edMelody.length;
|
||||||
|
edMelody=edMelody.filter(function(m){return m.t<lo2||m.t>hi2;});
|
||||||
|
edSelRange=null;
|
||||||
|
edDraw();
|
||||||
|
document.getElementById("ed-status").textContent="Deleted "+(before-edMelody.length)+" notes";
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
if(e.key==="ArrowLeft"&&edSelected>=0){
|
if(e.key==="ArrowLeft"&&edSelected>=0){
|
||||||
var snap=edIsClassical?(60/edBpm/4):0.25;
|
var snap=edIsClassical?(60/edBpm/4):0.25;
|
||||||
edMelody[edSelected].t=Math.max(0,parseFloat((edMelody[edSelected].t-snap).toFixed(4)));
|
edMelody[edSelected].t=Math.max(0,parseFloat((edMelody[edSelected].t-snap).toFixed(4)));
|
||||||
@@ -5048,8 +5237,8 @@ function edKeyDown(e){
|
|||||||
}
|
}
|
||||||
if(e.key==="ArrowRight"&&edSelected>=0){
|
if(e.key==="ArrowRight"&&edSelected>=0){
|
||||||
var snap2=edIsClassical?(60/edBpm/4):0.25;
|
var snap2=edIsClassical?(60/edBpm/4):0.25;
|
||||||
var maxT=edIsClassical?9999:3.99;
|
var maxT2=edIsClassical?9999:3.99;
|
||||||
edMelody[edSelected].t=Math.min(maxT,parseFloat((edMelody[edSelected].t+snap2).toFixed(4)));
|
edMelody[edSelected].t=Math.min(maxT2,parseFloat((edMelody[edSelected].t+snap2).toFixed(4)));
|
||||||
edMelody.sort(function(a,b){return a.t-b.t;});
|
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||||
edDraw();e.preventDefault();
|
edDraw();e.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -5061,6 +5250,22 @@ function edKeyDown(e){
|
|||||||
edMelody[edSelected].l=Math.min(4,edMelody[edSelected].l+1);
|
edMelody[edSelected].l=Math.min(4,edMelody[edSelected].l+1);
|
||||||
edDraw();e.preventDefault();
|
edDraw();e.preventDefault();
|
||||||
}
|
}
|
||||||
|
// D key — toggle/set duration on selected note
|
||||||
|
if(e.key==="d"&&edSelected>=0&&!e.ctrlKey&&!e.metaKey){
|
||||||
|
var n=edMelody[edSelected];
|
||||||
|
var snapD=edIsClassical?(60/edBpm/4):0.25;
|
||||||
|
if(!n.d||n.d<=0){
|
||||||
|
n.d=snapD; // add default duration
|
||||||
|
}else{
|
||||||
|
n.d=parseFloat((n.d+snapD).toFixed(4)); // increase by one grid unit
|
||||||
|
}
|
||||||
|
edDraw();e.preventDefault();
|
||||||
|
}
|
||||||
|
// Shift+D — remove duration (make instant)
|
||||||
|
if(e.key==="D"&&edSelected>=0){
|
||||||
|
delete edMelody[edSelected].d;
|
||||||
|
edDraw();e.preventDefault();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup editor canvas events
|
// Setup editor canvas events
|
||||||
@@ -5126,7 +5331,7 @@ function edGetAllSaved(){
|
|||||||
function edLoadSaved(name){var all=edGetAllSaved();return all[name]||null;}
|
function edLoadSaved(name){var all=edGetAllSaved();return all[name]||null;}
|
||||||
function edSave(){
|
function edSave(){
|
||||||
var all=edGetAllSaved();
|
var all=edGetAllSaved();
|
||||||
var entry={melody:edMelody.map(function(m){return{t:m.t,l:m.l};}),bpm:edBpm,isClassical:edIsClassical};
|
var entry={melody:edMelody.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;}),bpm:edBpm,isClassical:edIsClassical};
|
||||||
if(!edIsClassical)entry.bars=edBars;
|
if(!edIsClassical)entry.bars=edBars;
|
||||||
all[edPiece.n]=entry;
|
all[edPiece.n]=entry;
|
||||||
try{localStorage.setItem(ED_STORAGE_KEY,JSON.stringify(all));}catch(e){}
|
try{localStorage.setItem(ED_STORAGE_KEY,JSON.stringify(all));}catch(e){}
|
||||||
@@ -5137,9 +5342,9 @@ function edReset(){
|
|||||||
var all=edGetAllSaved();delete all[edPiece.n];
|
var all=edGetAllSaved();delete all[edPiece.n];
|
||||||
try{localStorage.setItem(ED_STORAGE_KEY,JSON.stringify(all));}catch(e){}
|
try{localStorage.setItem(ED_STORAGE_KEY,JSON.stringify(all));}catch(e){}
|
||||||
if(edIsClassical){
|
if(edIsClassical){
|
||||||
edMelody=edPiece.melody.map(function(m){return{t:m.t,l:m.l};});
|
edMelody=edPiece.melody.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;});
|
||||||
}else{
|
}else{
|
||||||
edMelody=edPiece.p.map(function(m){return{t:m.t,l:m.l};});
|
edMelody=edPiece.p.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;});
|
||||||
edBars=edPiece.bars||8;
|
edBars=edPiece.bars||8;
|
||||||
}
|
}
|
||||||
edBpm=edPiece.bpm;
|
edBpm=edPiece.bpm;
|
||||||
@@ -5147,7 +5352,8 @@ function edReset(){
|
|||||||
edSelected=-1;edDraw();
|
edSelected=-1;edDraw();
|
||||||
}
|
}
|
||||||
function edExport(){
|
function edExport(){
|
||||||
var data=JSON.stringify({name:edPiece.n,bpm:edBpm,type:edIsClassical?"classical":"pattern",melody:edMelody},null,2);
|
var exportMelody=edMelody.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;});
|
||||||
|
var data=JSON.stringify({name:edPiece.n,bpm:edBpm,type:edIsClassical?"classical":"pattern",melody:exportMelody},null,2);
|
||||||
navigator.clipboard.writeText(data).then(function(){
|
navigator.clipboard.writeText(data).then(function(){
|
||||||
document.getElementById("ed-status").textContent="Copied to clipboard!";
|
document.getElementById("ed-status").textContent="Copied to clipboard!";
|
||||||
}).catch(function(){
|
}).catch(function(){
|
||||||
@@ -5169,22 +5375,24 @@ function edToggleCode(){
|
|||||||
function edUpdateCode(){
|
function edUpdateCode(){
|
||||||
var ta=document.getElementById("ed-code");
|
var ta=document.getElementById("ed-code");
|
||||||
if(edIsClassical){
|
if(edIsClassical){
|
||||||
// Show melody array as JS
|
|
||||||
var lines=edMelody.map(function(m){
|
var lines=edMelody.map(function(m){
|
||||||
return" {t:"+m.t.toFixed(3)+", l:"+m.l+"}";
|
var s=" {t:"+m.t.toFixed(3)+", l:"+m.l;
|
||||||
|
if(m.d>0)s+=", d:"+m.d.toFixed(3);
|
||||||
|
return s+"}";
|
||||||
});
|
});
|
||||||
ta.value="// "+edPiece.n+" — melody ("+edMelody.length+" notes)\n"+
|
ta.value="// "+edPiece.n+" — melody ("+edMelody.length+" notes)\n"+
|
||||||
"// BPM: "+edBpm+", Duration: "+(edPiece.duration||"?")+"s\n"+
|
"// BPM: "+edBpm+", Duration: "+(edPiece.duration||"?")+"s\n"+
|
||||||
"// Lanes: 0=Red 1=Yellow 2=Blue 3=Green 4=Kick\n"+
|
"// Lanes: 0=Red 1=Yellow 2=Blue 3=Green 4=Kick | d=duration(s)\n"+
|
||||||
"melody: [\n"+lines.join(",\n")+"\n]";
|
"melody: [\n"+lines.join(",\n")+"\n]";
|
||||||
}else{
|
}else{
|
||||||
// Show pattern array as JS
|
|
||||||
var lines2=edMelody.map(function(m){
|
var lines2=edMelody.map(function(m){
|
||||||
return" {t:"+m.t+", l:"+m.l+"}";
|
var s=" {t:"+m.t+", l:"+m.l;
|
||||||
|
if(m.d>0)s+=", d:"+m.d;
|
||||||
|
return s+"}";
|
||||||
});
|
});
|
||||||
ta.value="// "+edPiece.n+" — pattern ("+edMelody.length+" notes, repeats "+edBars+"x)\n"+
|
ta.value="// "+edPiece.n+" — pattern ("+edMelody.length+" notes, repeats "+edBars+"x)\n"+
|
||||||
"// BPM: "+edBpm+", Bars: "+edBars+"\n"+
|
"// BPM: "+edBpm+", Bars: "+edBars+"\n"+
|
||||||
"// t = beat offset (0-4), Lanes: 0=Red 1=Yellow 2=Blue 3=Green 4=Kick\n"+
|
"// t = beat offset (0-4), Lanes: 0=Red 1=Yellow 2=Blue 3=Green 4=Kick | d=duration\n"+
|
||||||
"p: [\n"+lines2.join(",\n")+"\n]";
|
"p: [\n"+lines2.join(",\n")+"\n]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5199,7 +5407,7 @@ function edApplyCode(){
|
|||||||
var arrStr="["+match[1].replace(/(\w+)\s*:/g,'"$1":')+"]";
|
var arrStr="["+match[1].replace(/(\w+)\s*:/g,'"$1":')+"]";
|
||||||
var arr=JSON.parse(arrStr);
|
var arr=JSON.parse(arrStr);
|
||||||
edMelody=arr.filter(function(m){return typeof m.t==="number"&&typeof m.l==="number";})
|
edMelody=arr.filter(function(m){return typeof m.t==="number"&&typeof m.l==="number";})
|
||||||
.map(function(m){return{t:m.t,l:m.l};});
|
.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;});
|
||||||
edMelody.sort(function(a,b){return a.t-b.t;});
|
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||||
edSelected=-1;edDraw();
|
edSelected=-1;edDraw();
|
||||||
document.getElementById("ed-status").textContent="Applied! "+edMelody.length+" notes loaded from code.";
|
document.getElementById("ed-status").textContent="Applied! "+edMelody.length+" notes loaded from code.";
|
||||||
@@ -5223,7 +5431,7 @@ function edDoImport(){
|
|||||||
var arr=Array.isArray(data)?data:(data.melody||data.p||[]);
|
var arr=Array.isArray(data)?data:(data.melody||data.p||[]);
|
||||||
if(!Array.isArray(arr)||arr.length===0){document.getElementById("ed-import-status").textContent="No notes found";return;}
|
if(!Array.isArray(arr)||arr.length===0){document.getElementById("ed-import-status").textContent="No notes found";return;}
|
||||||
edMelody=arr.filter(function(m){return typeof m.t==="number"&&typeof m.l==="number";})
|
edMelody=arr.filter(function(m){return typeof m.t==="number"&&typeof m.l==="number";})
|
||||||
.map(function(m){return{t:m.t,l:m.l};});
|
.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;});
|
||||||
edMelody.sort(function(a,b){return a.t-b.t;});
|
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||||
if(data.bpm)edBpm=data.bpm;
|
if(data.bpm)edBpm=data.bpm;
|
||||||
document.getElementById("ed-bpm").value=edBpm;
|
document.getElementById("ed-bpm").value=edBpm;
|
||||||
@@ -5276,7 +5484,7 @@ function edLoadEditsFile(e){
|
|||||||
localStorage.setItem(ED_STORAGE_KEY,JSON.stringify(all));
|
localStorage.setItem(ED_STORAGE_KEY,JSON.stringify(all));
|
||||||
document.getElementById("ed-status").textContent="Loaded "+count+" edit(s) from file";
|
document.getElementById("ed-status").textContent="Loaded "+count+" edit(s) from file";
|
||||||
// Refresh current editor view if open
|
// Refresh current editor view if open
|
||||||
if(edPiece){var saved=edLoadSaved(edPiece.n);if(saved){edMelody=saved.melody.map(function(m){return{t:m.t,l:m.l};});edBpm=saved.bpm||edBpm;document.getElementById("ed-bpm").value=edBpm;edDraw();}}
|
if(edPiece){var saved=edLoadSaved(edPiece.n);if(saved){edMelody=saved.melody.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;});edBpm=saved.bpm||edBpm;document.getElementById("ed-bpm").value=edBpm;edDraw();}}
|
||||||
}catch(err){document.getElementById("ed-status").textContent="Error: "+err.message;}
|
}catch(err){document.getElementById("ed-status").textContent="Error: "+err.message;}
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
|
|||||||
Reference in New Issue
Block a user