char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
844055b
main
1import type { JSX } from "@char/aftercare/jsx-runtime" ; 2import { voiceTrack } from "../project/harmony.ts" ; 3import { projectKey } from "../project/model.ts" ; 4import { project } from "../project/store.ts" ; 5import { analyze , prettyRoman , romanOf } from "../theory/analysis.ts" ; 6import { type Chord , chordFrom , chordName } from "../theory/chord.ts" ; 7import { MODES } from "../theory/key.ts" ; 8import { mod , pcOf , spellPc } from "../theory/pitch.ts" ; 9import { toggleSeventh } from "../theory/transform.ts" ; 10import { nextChordAt , paletteTrack , previewChord , writeChordFromPalette } from "./actions.ts" ; 11import { watch } from "./dom.tsx" ; 12import { originClass } from "./origin.ts" ; 13import { activeTrackId , cursor , paletteSevenths } from "./state.ts" ; 14 15const SVG = "http://www.w3.org/2000/svg" ; 16const svg = ( tag :string , attrs :Record < string , string | number >, ...children :Node []) => { 17const el = document . createElementNS ( SVG , tag ); 18for ( const [ k , v ] of Object . entries ( attrs )) el . setAttribute ( k , String ( v )); 19el . append ( ...children ); 20return el ; 21}; 22 23const RINGS = { major :[ 72 , 118 ], minor :[ 38 , 72 ] } as const ; 24 25/** a twelfth of a ring, `step` fifths clockwise from the top */ 26function wedge ( step :number , [ inner , outer ] :readonly [ number , number ]) { 27const at = ( r :number , deg :number ) => { 28const a = (( deg - 90 ) * Math . PI ) / 180 ; 29return ` ${( r * Math . cos ( a )). toFixed ( 2 )} ${( r * Math . sin ( a )). toFixed ( 2 )} ` ; 30}; 31const [ from , to ] = [ step * 30 - 15 , step * 30 + 15 ]; 32return ( 33`M ${ at ( outer , from )} A ${ outer } ${ outer } 0 0 1 ${ at ( outer , to )} ` + 34`L ${ at ( inner , to )} A ${ inner } ${ inner } 0 0 0 ${ at ( inner , from )} Z` 35); 36} 37 38/** 39* the circle of fifths, turned so the current chord (the one at the cursor, or else the one 40* before it) is at the top: what's a fifth either way of it, and the relative minors inside 41*/ 42export function Circle () :JSX . Element { 43const circle = svg ( "svg" , { class :"circle" , viewBox :"-120 -120 240 240" }); 44 45watch ( circle , [ project , cursor , activeTrackId , paletteSevenths ], () => { 46const p = project . get (); 47const key = projectKey ( p ); 48const track = paletteTrack ( p ); 49const voiced = track ?voiceTrack ( track , key ) :[]; 50const at = cursor . get (); 51const current = ( 52voiced . find ( v => v . event . start === at ) ?? voiced . findLast ( v => v . event . start < at ) 53)?. chord ; 54 55// the major key at the top: the chord's own, or for a minor chord its relative major, and 56// for a diminished one the key it's the leading-tone chord of 57const tones = current ?. tones ?? ( MODES [ key . mode ]. steps . includes ( 3 ) ?[ 0 , 3 , 7 ] :[ 0 , 4 , 7 ]); 58const root = current ?pcOf ( current . root ) :pcOf ( key . tonic ); 59const minor = tones . includes ( 3 ) && tones . includes ( 7 ) && ! tones . includes ( 4 ); 60const diminished = tones . includes ( 3 ) && tones . includes ( 6 ); 61const top = mod ( root + ( minor ?3 :diminished ?1 :0 ), 12 ); 62const here = minor ?RINGS . minor :diminished ?undefined :RINGS . major ; 63 64const chip = ( triad :Chord , step :number , ring :readonly [ number , number ]) => { 65// majors take whichever seventh the key has (Cmaj7, but G7 and D7 in C); minors always the 66// minor one, as a borrowed Fm in C is Fm7 rather than the Fm(maj7) the key would make it 67const seventh = 68paletteSevenths . get () && 69( ring === RINGS . minor 70 ?chordFrom ( triad . root , [ 0 , 3 , 7 , 10 ]) 71 :toggleSeventh ( triad , key )); 72const chord = seventh || triad ; 73const roman = romanOf ( chord , key ); 74const middle = ( ring [ 0 ] + ring [ 1 ]) / 2 ; 75const a = (( step * 30 - 90 ) * Math . PI ) / 180 ; 76const [ x , y ] = [ middle * Math . cos ( a ), middle * Math . sin ( a )]; 77const cls = `circle-chord ${ ring === RINGS . minor ? "minor " : "" } ${ originClass ( analyze ( chord , key ))} ` ; 78const g = svg ( 79"g" , 80{ class :` ${ cls } ${ ! step && ring === here ? " here" : "" } ` }, 81svg ( "path" , { d :wedge ( step , ring ) }), 82svg ( "text" , { x, y :y - 1 , class :"name" }, document . createTextNode ( chordName ( chord ))), 83svg ( 84"text" , 85{ x, y :y + 10 , class :"roman" }, 86document . createTextNode ( prettyRoman ( roman )), 87), 88); 89// the second click of a double-click would only preview on top of the write 90g . addEventListener ( 91"click" , 92e => ( e as MouseEvent ). detail < 2 && previewChord ( roman , nextChordAt ()), 93); 94g . addEventListener ( "dblclick" , () => writeChordFromPalette ( roman , nextChordAt ())); 95return g ; 96}; 97 98const chips = []; 99for ( let step = 0 ; step < 12 ; step ++ ) { 100const major = mod ( top + 7 * step , 12 ); 101const relative = mod ( major + 9 , 12 ); 102chips . push ( 103chip ( chordFrom ( spellPc ( major , key . tonic ), [ 0 , 4 , 7 ]) ! , step , RINGS . major ), 104chip ( chordFrom ( spellPc ( relative , key . tonic ), [ 0 , 3 , 7 ]) ! , step , RINGS . minor ), 105); 106} 107circle . replaceChildren ( ...chips ); 108}); 109 110return circle as JSX . Element ; 111}