char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
ad1ae60
main
1import type { JSX } from "@char/aftercare/jsx-runtime" ; 2import { voiceTrack } from "../project/harmony.ts" ; 3import { canonBeat , insertLeadIn , projectKey } from "../project/model.ts" ; 4import { project } from "../project/store.ts" ; 5import { prettyRoman } from "../theory/analysis.ts" ; 6import { type Chord , chordName , chordSymbol } from "../theory/chord.ts" ; 7import { connections } from "../theory/connect.ts" ; 8import { parallel , relative , suspend , toggleSeventh , tritoneSub } from "../theory/transform.ts" ; 9import { invertVoicing } from "../theory/voicing.ts" ; 10import { leadInto , revoice , transformChord , unpin } from "./actions.ts" ; 11import { disclosure , watch } from "./dom.tsx" ; 12import { activeTrackId , chordPanelOpen , cursor } from "./state.ts" ; 13 14const button = ( label :string , result :string | undefined , run :() => void , title = "" ) => ( 15< button 16type = "button" 17class = "chord-panel-action" 18disabled = { result === undefined } 19title = { title } 20_onclick = { run } 21> 22{ label } 23{ result ?< b >{ result}< / b> : undefined} 24</ button > 25); 26 27const group = ( label :string , buttons :JSX . Element []) => ( 28< div class = "chord-panel-group" > 29< span class = "chord-panel-label" >{ label}< / span> 30< div class = "chord-panel-buttons" >{ buttons }< /div> 31</ div > 32); 33 34const names = ( chords : Chord | Chord [] | undefined ) => 35chords && [ chords ]. flat (). map ( chordName ). join ( " " ); 36 37/** tools for the chord at the cursor: its voicing, variations on it, and ways into it */ 38export function ChordPanel () : JSX . Element { 39const panel = (< div class = "chord-panel-body" / >) as HTMLElement ; 40const name = (< div class = "chord-panel-name" / >) as HTMLElement ; 41 42watch ( panel , [ project , cursor , activeTrackId , chordPanelOpen ], () => { 43if ( ! chordPanelOpen . get ()) { 44name . replaceChildren (); 45return panel . replaceChildren (); 46} 47const p = project . get (); 48const track = p . tracks . find ( t => t . id === activeTrackId . get ()); 49const key = projectKey ( p ); 50const voiced = track ?. kind === "chords" ? voiceTrack ( track , key ) : []; 51const v = voiced . find ( v => v . event . start === cursor . get ()); 52if ( ! v || track ?. kind !== "chords" ) { 53name . replaceChildren (); 54panel . replaceChildren (< span class = "chord-panel-empty" > select a chord to change it < /span>); 55return ; 56} 57 58const [ trackId , id , c ] = [ track . id , v . event . id , v . chord ]; 59const voicing = ( label : string , change : ( notes : number []) => number []) => 60button ( label , "" , () => revoice ( trackId , id , change )); 61const octave = 62( by : number ) => 63([ bass , ... upper ] : number []) => [ bass , ... upper . map ( n => n + by )]; 64const change = ( label : string , f : ( c : Chord ) => Chord | undefined , title ?: string ) => 65button ( label , names ( c && f ( c )), () => transformChord ( trackId , id , f ), title ); 66// the ways in from the chord before, where there's room for them before this one 67const i = voiced . indexOf ( v ); 68const prev = voiced [ i - 1 ]; 69const before = prev && canonBeat ( prev . event . start + prev . event . dur ) === v . event . start ; 70const routes = c ? connections ( before ? prev . chord : undefined , c ) : []; 71const fits = ( chords : Chord []) => 72insertLeadIn ( structuredClone ( track ), id , chords . map ( chordSymbol ), p . grid ); 73const connect = routes 74. filter ( r => fits ( r . chords )) 75. slice ( 0 , 6 ) 76. map ( r => button ( r . name , names ( r . chords ), () => leadInto ( trackId , id , r . chords ))); 77const adds = c && ( toggleSeventh ( c , key )?. tones . length ?? 0 ) > c . tones . length ; 78const seventh = adds ? "+ 7th" : "− 7th" ; 79 80name . replaceChildren ( 81< b >{ v . label }</ b >, 82< span >{ v . analysis ? prettyRoman ( v . analysis . roman ) : "" }</ span >, 83); 84panel . replaceChildren ( 85group ( "voicing" , [ 86voicing ( "inversion ↓" , notes => invertVoicing ( notes , - 1 )), 87voicing ( "inversion ↑" , notes => invertVoicing ( notes , 1 )), 88voicing ( "octave ↓" , octave ( - 12 )), 89voicing ( "octave ↑" , octave ( 12 )), 90button ( 91"unpin" , 92v . pinned ? "" : undefined , 93() => unpin ( trackId , id ), 94"back to voicing it from its neighbours" , 95), 96]), 97... ( c 98? [ 99group ( "alterations" , [ 100change ( seventh , c => toggleSeventh ( c , key )), 101change ( "sus2" , c => suspend ( c , key , 2 )), 102change ( "sus4" , c => suspend ( c , key , 5 )), 103change ( "parallel" , parallel , "the same chord with the other third" ), 104change ( "relative" , relative , "the chord a minor third away, sharing most notes" ), 105change ( "tritone sub" , tritoneSub , "a dominant a tritone away" ), 106]), 107... ( connect . length 108? [ group ( before ? `from ${ prev . label } ` : "lead in" , connect )] 109: []), 110] 111: []), 112); 113}); 114 115return ( 116< section class = "chord-panel" dataset = {{ open : chordPanelOpen . derive ( String ) }}> 117{ disclosure ( chordPanelOpen , "chord" )} 118{ name } 119{ panel } 120< / section> 121); 122}