Merge pull request #36 from outis1one/clDemoe/check-editor-visibility-0nTHw
Add note properties panel and human-readable pitch/timing to editor
This commit is contained in:
+187
-10
@@ -214,6 +214,39 @@ canvas{display:block;}
|
||||
Click=select | Dbl-click=add note | Drag=move | Drag edge=resize | 1-5=note value | Shift+↑↓=pitch | ↑↓=lane | ←→=nudge time | D=+dur | Shift+D=-dur | Shift+drag=select range | Ctrl+C/V=copy/paste | Del=delete | Scroll=pan | Click header=set start | Dbl-click header=play from there
|
||||
<span id="ed-mode-label" style="margin-left:12px;color:#805ad5;"></span>
|
||||
</div>
|
||||
<!-- Note Properties Panel (appears when a note is selected) -->
|
||||
<div id="ed-note-panel" style="display:none;padding:6px 12px;background:#0d0d18;border-bottom:1px solid #333;flex-shrink:0;">
|
||||
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;">
|
||||
<span style="color:#aaa;font-size:12px;font-weight:bold;">Selected Note:</span>
|
||||
<label style="color:#aaa;font-size:12px;">Pitch:
|
||||
<input type="text" id="ed-note-pitch" placeholder="e.g. C4, D#5, Bb3" style="width:70px;background:#1a1a2e;color:#ff0;border:1px solid #444;padding:2px 4px;border-radius:3px;font-family:monospace;font-size:12px;" onchange="edSetPitch(this.value)">
|
||||
</label>
|
||||
<label style="color:#aaa;font-size:12px;">Duration:
|
||||
<select id="ed-note-dur" style="background:#1a1a2e;color:#fff;border:1px solid #444;padding:2px;border-radius:3px;font-size:12px;" onchange="edSetDuration(this.value)">
|
||||
<option value="0">— (instant)</option>
|
||||
<option value="whole">𝅝 Whole</option>
|
||||
<option value="dotted-half">𝅗𝅥. Dotted Half</option>
|
||||
<option value="half">𝅗𝅥 Half</option>
|
||||
<option value="dotted-quarter">♩. Dotted Quarter</option>
|
||||
<option value="quarter">♩ Quarter</option>
|
||||
<option value="dotted-eighth">♪. Dotted Eighth</option>
|
||||
<option value="eighth">♪ Eighth</option>
|
||||
<option value="sixteenth">♬ Sixteenth</option>
|
||||
<option value="32nd">♬♬ 32nd</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style="color:#aaa;font-size:12px;">Lane:
|
||||
<select id="ed-note-lane" style="background:#1a1a2e;color:#fff;border:1px solid #444;padding:2px;border-radius:3px;font-size:12px;" onchange="edSetLane(this.value)">
|
||||
<option value="0" style="color:#e53e3e">0 - Red</option>
|
||||
<option value="1" style="color:#d69e2e">1 - Yellow</option>
|
||||
<option value="2" style="color:#3182ce">2 - Blue</option>
|
||||
<option value="3" style="color:#38a169">3 - Green</option>
|
||||
<option value="4" style="color:#805ad5">4 - Kick</option>
|
||||
</select>
|
||||
</label>
|
||||
<span id="ed-note-info" style="color:#666;font-size:11px;font-family:monospace;"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ed-code-panel" style="display:none;padding:8px 12px;background:#0a0a14;border-bottom:1px solid #333;overflow-y:auto;flex-shrink:0;max-height:45vh;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
|
||||
<span style="color:#888;font-size:11px;">JavaScript — edit and click "Apply Code" to update the timeline. Drag bottom edge to resize.</span>
|
||||
@@ -4912,6 +4945,112 @@ function edFreqToNote(freq){
|
||||
return best;
|
||||
}
|
||||
|
||||
// Note name to frequency (reverse of edFreqToNote)
|
||||
// Accepts: C4, D#5, Db3, Eb4, F#3, Bb2, etc.
|
||||
function edNoteToFreq(name){
|
||||
if(!name||name==="?")return 0;
|
||||
name=name.trim();
|
||||
var match=name.match(/^([A-Ga-g])(#{1,2}|b{1,2}|♯|♭)?(\d)$/);
|
||||
if(!match)return 0;
|
||||
var letter=match[1].toUpperCase();
|
||||
var accidental=match[2]||"";
|
||||
var octave=parseInt(match[3]);
|
||||
var semitones={"C":0,"D":2,"E":4,"F":5,"G":7,"A":9,"B":11}[letter];
|
||||
if(semitones===undefined)return 0;
|
||||
if(accidental==="#"||accidental==="♯")semitones+=1;
|
||||
else if(accidental==="##")semitones+=2;
|
||||
else if(accidental==="b"||accidental==="♭")semitones-=1;
|
||||
else if(accidental==="bb")semitones-=2;
|
||||
// MIDI note number: C4=60, A4=69
|
||||
var midi=(octave+1)*12+semitones;
|
||||
return parseFloat((440*Math.pow(2,(midi-69)/12)).toFixed(2));
|
||||
}
|
||||
|
||||
// Duration name to seconds
|
||||
function edDurNameToSec(name){
|
||||
var q=edQuarterSec();
|
||||
var map={"whole":q*4,"dotted-half":q*3,"half":q*2,"dotted-quarter":q*1.5,"quarter":q,"dotted-eighth":q*0.75,"eighth":q/2,"sixteenth":q/4,"32nd":q/8};
|
||||
return map[name]||0;
|
||||
}
|
||||
|
||||
// Duration seconds to name (for dropdown selection)
|
||||
function edDurSecToName(durSec){
|
||||
if(!durSec||durSec<=0)return "0";
|
||||
var q=edQuarterSec();
|
||||
var ratio=durSec/q;
|
||||
if(ratio>=3.5) return "whole";
|
||||
if(ratio>=2.5) return "dotted-half";
|
||||
if(ratio>=1.8) return "half";
|
||||
if(ratio>=1.3) return "dotted-quarter";
|
||||
if(ratio>=0.8) return "quarter";
|
||||
if(ratio>=0.6) return "dotted-eighth";
|
||||
if(ratio>=0.4) return "eighth";
|
||||
if(ratio>=0.2) return "sixteenth";
|
||||
return "32nd";
|
||||
}
|
||||
|
||||
// Update note properties panel when selection changes
|
||||
function edUpdateNotePanel(){
|
||||
var panel=document.getElementById("ed-note-panel");
|
||||
if(edSelected<0||edSelected>=edMelody.length){
|
||||
panel.style.display="none";
|
||||
return;
|
||||
}
|
||||
panel.style.display="block";
|
||||
var m=edMelody[edSelected];
|
||||
// Pitch input
|
||||
var pitchInput=document.getElementById("ed-note-pitch");
|
||||
pitchInput.value=m.f?edFreqToNote(m.f):"";
|
||||
// Duration dropdown
|
||||
var durSel=document.getElementById("ed-note-dur");
|
||||
durSel.value=edDurSecToName(m.d||0);
|
||||
// Lane dropdown
|
||||
document.getElementById("ed-note-lane").value=m.l;
|
||||
// Info line
|
||||
var info="t="+m.t.toFixed(3)+"s";
|
||||
if(m.f)info+=" | freq="+m.f.toFixed(1)+"Hz";
|
||||
if(m.d>0)info+=" | dur="+m.d.toFixed(3)+"s";
|
||||
document.getElementById("ed-note-info").textContent=info;
|
||||
// Resize canvas since panel may have appeared
|
||||
setTimeout(edResize,30);
|
||||
}
|
||||
|
||||
// Set pitch from note panel input
|
||||
function edSetPitch(val){
|
||||
if(edSelected<0)return;
|
||||
var freq=edNoteToFreq(val);
|
||||
if(freq>0){
|
||||
edMelody[edSelected].f=freq;
|
||||
}else if(val.trim()===""){
|
||||
delete edMelody[edSelected].f;
|
||||
}else{
|
||||
document.getElementById("ed-note-info").textContent="Invalid pitch — use e.g. C4, D#5, Bb3";
|
||||
return;
|
||||
}
|
||||
edDraw();
|
||||
edUpdateNotePanel();
|
||||
}
|
||||
|
||||
// Set duration from note panel dropdown
|
||||
function edSetDuration(val){
|
||||
if(edSelected<0)return;
|
||||
if(val==="0"){
|
||||
delete edMelody[edSelected].d;
|
||||
}else{
|
||||
edMelody[edSelected].d=parseFloat(edDurNameToSec(val).toFixed(4));
|
||||
}
|
||||
edDraw();
|
||||
edUpdateNotePanel();
|
||||
}
|
||||
|
||||
// Set lane from note panel dropdown
|
||||
function edSetLane(val){
|
||||
if(edSelected<0)return;
|
||||
edMelody[edSelected].l=parseInt(val);
|
||||
edDraw();
|
||||
edUpdateNotePanel();
|
||||
}
|
||||
|
||||
function getEdCanvas(){return document.getElementById("ed-canvas");}
|
||||
function getEdCtx(){return getEdCanvas().getContext("2d");}
|
||||
function edPxPerUnit(){return parseInt(document.getElementById("ed-zoom").value);}
|
||||
@@ -5235,6 +5374,7 @@ function edDraw(){
|
||||
statusTxt+=" | Click=add, Shift+drag=select range";
|
||||
}
|
||||
document.getElementById("ed-status").textContent=statusTxt;
|
||||
edUpdateNotePanel();
|
||||
}
|
||||
|
||||
function edHitTest(x,y){
|
||||
@@ -5733,8 +5873,14 @@ function edReset(){
|
||||
edSelected=-1;edDraw();
|
||||
}
|
||||
function edExport(){
|
||||
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);
|
||||
var laneNames=["Red","Yellow","Blue","Green","Kick"];
|
||||
var exportMelody=edMelody.map(function(m){
|
||||
var n={t:m.t,l:m.l,lane:laneNames[m.l]};
|
||||
if(m.f){n.f=m.f;n.pitch=edFreqToNote(m.f);}
|
||||
if(m.d>0){n.d=m.d;var di=edDurInfo(m.d);n.duration=di.name;}
|
||||
return n;
|
||||
});
|
||||
var data=JSON.stringify({name:edPiece.n,bpm:edBpm,timeSig:edTimeSig[0]+"/"+edTimeSig[1],type:edIsClassical?"classical":"pattern",melody:exportMelody},null,2);
|
||||
navigator.clipboard.writeText(data).then(function(){
|
||||
document.getElementById("ed-status").textContent="Copied to clipboard!";
|
||||
}).catch(function(){
|
||||
@@ -5764,26 +5910,38 @@ function edToggleCode(){
|
||||
}
|
||||
function edUpdateCode(){
|
||||
var ta=document.getElementById("ed-code");
|
||||
var laneNames=["Red","Yel","Blu","Grn","Kick"];
|
||||
if(edIsClassical){
|
||||
var lines=edMelody.map(function(m){
|
||||
var s=" {t:"+m.t.toFixed(3)+", l:"+m.l;
|
||||
if(m.f)s+=", f:"+m.f.toFixed(2);
|
||||
if(m.d>0)s+=", d:"+m.d.toFixed(3);
|
||||
return s+"}";
|
||||
s+="}";
|
||||
// Human-readable comment
|
||||
var comment=" // "+laneNames[m.l];
|
||||
if(m.f)comment+=" "+edFreqToNote(m.f);
|
||||
if(m.d>0){var di=edDurInfo(m.d);comment+=" "+di.sym+" "+di.name;}
|
||||
return s+","+comment;
|
||||
});
|
||||
ta.value="// "+edPiece.n+" — melody ("+edMelody.length+" notes)\n"+
|
||||
"// BPM: "+edBpm+", Duration: "+(edPiece.duration||"?")+"s\n"+
|
||||
"// Lanes: 0=Red 1=Yellow 2=Blue 3=Green 4=Kick | d=duration(s)\n"+
|
||||
"melody: [\n"+lines.join(",\n")+"\n]";
|
||||
"// BPM: "+edBpm+", Time Sig: "+edTimeSig[0]+"/"+edTimeSig[1]+", Duration: "+(edPiece.duration||"?")+"s\n"+
|
||||
"// Lanes: 0=Red 1=Yellow 2=Blue 3=Green 4=Kick\n"+
|
||||
"// f=frequency(Hz) → pitch name, d=duration(s) → note value\n"+
|
||||
"// Pitch: edit f values or use note names in Apply Code (e.g. f:\"C4\" → converted)\n"+
|
||||
"melody: [\n"+lines.join("\n")+"\n]";
|
||||
}else{
|
||||
var lines2=edMelody.map(function(m){
|
||||
var s=" {t:"+m.t+", l:"+m.l;
|
||||
if(m.d>0)s+=", d:"+m.d;
|
||||
return s+"}";
|
||||
s+="}";
|
||||
var comment=" // "+laneNames[m.l];
|
||||
if(m.d>0){var di=edDurInfo(m.d);comment+=" "+di.sym+" "+di.name;}
|
||||
return s+","+comment;
|
||||
});
|
||||
ta.value="// "+edPiece.n+" — pattern ("+edMelody.length+" notes, repeats "+edBars+"x)\n"+
|
||||
"// BPM: "+edBpm+", Bars: "+edBars+"\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]";
|
||||
}
|
||||
}
|
||||
function edApplyCode(){
|
||||
@@ -5797,7 +5955,18 @@ function edApplyCode(){
|
||||
var arrStr="["+match[1].replace(/(\w+)\s*:/g,'"$1":')+"]";
|
||||
var arr=JSON.parse(arrStr);
|
||||
edMelody=arr.filter(function(m){return typeof m.t==="number"&&typeof m.l==="number";})
|
||||
.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;});
|
||||
.map(function(m){
|
||||
var n={t:m.t,l:m.l};
|
||||
if(m.d>0)n.d=m.d;
|
||||
// Handle f as number (Hz) or string (note name like "C4")
|
||||
if(typeof m.f==="string"){
|
||||
var freq=edNoteToFreq(m.f);
|
||||
if(freq>0)n.f=freq;
|
||||
}else if(typeof m.f==="number"&&m.f>0){
|
||||
n.f=m.f;
|
||||
}
|
||||
return n;
|
||||
});
|
||||
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||
edSelected=-1;edDraw();
|
||||
document.getElementById("ed-status").textContent="Applied! "+edMelody.length+" notes loaded from code.";
|
||||
@@ -5821,7 +5990,15 @@ function edDoImport(){
|
||||
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;}
|
||||
edMelody=arr.filter(function(m){return typeof m.t==="number"&&typeof m.l==="number";})
|
||||
.map(function(m){var n={t:m.t,l:m.l};if(m.d>0)n.d=m.d;return n;});
|
||||
.map(function(m){
|
||||
var n={t:m.t,l:m.l};
|
||||
if(m.d>0)n.d=m.d;
|
||||
// Handle pitch as note name string or "pitch" field from export
|
||||
if(typeof m.f==="number"&&m.f>0){n.f=m.f;}
|
||||
else if(typeof m.f==="string"){var freq=edNoteToFreq(m.f);if(freq>0)n.f=freq;}
|
||||
else if(typeof m.pitch==="string"){var freq2=edNoteToFreq(m.pitch);if(freq2>0)n.f=freq2;}
|
||||
return n;
|
||||
});
|
||||
edMelody.sort(function(a,b){return a.t-b.t;});
|
||||
if(data.bpm)edBpm=data.bpm;
|
||||
document.getElementById("ed-bpm").value=edBpm;
|
||||
|
||||
Reference in New Issue
Block a user