char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
403dcb9
main
1import { Signal } from "@char/aftercare" ; 2import { canonBeat , type Track } from "../project/model.ts" ; 3import { project } from "../project/store.ts" ; 4 5export const ROW_H = 10 ; 6export const STRIP_H = 48 ; 7export const HEADER_W = 190 ; 8 9/** where typed chords go and playback starts, in beats. on chord tracks, the chord starting 10* here is the one that keyboard edits apply to */ 11export const cursor = new ( class extends Signal < number > { 12// it's compared against chord boundaries, so it lives on the same grid as they do 13override set ( beats :number ) { 14super . set ( canonBeat ( beats )); 15} 16})( 0 ); 17// "none" is null rather than undefined in signals: aftercare doesn't notify changes away from undefined 18export const activeTrackId = new Signal < string | null >( project . get (). tracks [ 0 ]?. id ?? null ); 19/** selected notes, on note tracks */ 20export const selection = new Signal <{ trackId :string ; ids :readonly string [] } | null >( null ); 21export type ChordEdit = { trackId :string ; initial :string ; selectAll :boolean }; 22export const chordEdit = new Signal < ChordEdit | null >( null ); 23export const pxPerBeat = new Signal ( 64 ); 24/** a signal kept in localStorage: for preferences about the app rather than a project */ 25function remembered < T > ( name :string , initial :T ) :Signal < T > { 26const key = `chord-pad/ui/ ${ name } ` ; 27let value = initial ; 28try { 29value = JSON . parse ( localStorage . getItem ( key ) ?? "null" ) ?? initial ; 30} catch { 31// unreadable: start over 32} 33const signal = new Signal ( value ); 34signal . subscribe ( v => localStorage . setItem ( key , JSON . stringify ( v ))); 35return signal ; 36} 37 38export const paletteSevenths = remembered ( "palette-sevenths" , false ); 39export const chordPanelOpen = remembered ( "chord-panel-open" , true ); 40export const mixtureOpen = remembered ( "mixture-open" , false ); 41 42export const rollHeight = ( t :Track ) :number => ( t . view . high - t . view . low + 1 ) * ROW_H ; 43export const laneHeight = ( t :Track ) :number => 44( t . kind === "chords" ?STRIP_H :0 ) + rollHeight ( t ); 45 46export const selectedIds = ( trackId :string ) :readonly string [] => { 47const sel = selection . get (); 48return sel ?. trackId === trackId ?sel . ids :[]; 49}; 50 51project . subscribe (( p , old ) => { 52if ( p . id === old . id ) return ; 53cursor . set ( 0 ); 54selection . set ( null ); 55chordEdit . set ( null ); 56activeTrackId . set ( p . tracks [ 0 ]?. id ?? null ); 57});