Add note properties panel and human-readable pitch/timing to editor

When a note is selected, a properties panel now appears with:
- Pitch input (type note names like C4, D#5, Bb3)
- Duration dropdown (whole, half, quarter, eighth, etc.)
- Lane selector dropdown

Code view now shows pitch names and duration names as comments
next to each note for easy reading. Export includes pitch/duration
names. Import and Apply Code accept note name strings in the f field.

https://claude.ai/code/session_018SvBLznyTuvoZAYCw9WwAA
This commit is contained in:
Claude
2026-04-02 15:45:28 +00:00
parent f331a384f8
commit 548476cb85
+187 -10
View File
@@ -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 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> <span id="ed-mode-label" style="margin-left:12px;color:#805ad5;"></span>
</div> </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 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;"> <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> <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; 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 getEdCanvas(){return document.getElementById("ed-canvas");}
function getEdCtx(){return getEdCanvas().getContext("2d");} function getEdCtx(){return getEdCanvas().getContext("2d");}
function edPxPerUnit(){return parseInt(document.getElementById("ed-zoom").value);} function edPxPerUnit(){return parseInt(document.getElementById("ed-zoom").value);}
@@ -5235,6 +5374,7 @@ function edDraw(){
statusTxt+=" | Click=add, Shift+drag=select range"; statusTxt+=" | Click=add, Shift+drag=select range";
} }
document.getElementById("ed-status").textContent=statusTxt; document.getElementById("ed-status").textContent=statusTxt;
edUpdateNotePanel();
} }
function edHitTest(x,y){ function edHitTest(x,y){
@@ -5733,8 +5873,14 @@ function edReset(){
edSelected=-1;edDraw(); edSelected=-1;edDraw();
} }
function edExport(){ 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 laneNames=["Red","Yellow","Blue","Green","Kick"];
var data=JSON.stringify({name:edPiece.n,bpm:edBpm,type:edIsClassical?"classical":"pattern",melody:exportMelody},null,2); 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(){ 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(){
@@ -5764,26 +5910,38 @@ function edToggleCode(){
} }
function edUpdateCode(){ function edUpdateCode(){
var ta=document.getElementById("ed-code"); var ta=document.getElementById("ed-code");
var laneNames=["Red","Yel","Blu","Grn","Kick"];
if(edIsClassical){ if(edIsClassical){
var lines=edMelody.map(function(m){ var lines=edMelody.map(function(m){
var s=" {t:"+m.t.toFixed(3)+", l:"+m.l; 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); 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"+ ta.value="// "+edPiece.n+" — melody ("+edMelody.length+" notes)\n"+
"// BPM: "+edBpm+", Duration: "+(edPiece.duration||"?")+"s\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 | d=duration(s)\n"+ "// Lanes: 0=Red 1=Yellow 2=Blue 3=Green 4=Kick\n"+
"melody: [\n"+lines.join(",\n")+"\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{ }else{
var lines2=edMelody.map(function(m){ var lines2=edMelody.map(function(m){
var s=" {t:"+m.t+", l:"+m.l; var s=" {t:"+m.t+", l:"+m.l;
if(m.d>0)s+=", d:"+m.d; 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"+ 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 | d=duration\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(){ function edApplyCode(){
@@ -5797,7 +5955,18 @@ 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){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;}); 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.";
@@ -5821,7 +5990,15 @@ 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){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;}); 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;