char-slop/chordpad.vibed.software

git clone https://git.t4t.associates/char-slop/chordpad.vibed.software

Charlotte Somui rework403dcb9

main
2.2 KiB57 linesraw
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
13  override set(beats: number) {
14    super.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> {
26  const key = `chord-pad/ui/${name}`;
27  let value = initial;
28  try {
29    value = JSON.parse(localStorage.getItem(key) ?? "null") ?? initial;
30  } catch {
31    // unreadable: start over
32  }
33  const signal = new Signal(value);
34  signal.subscribe(v => localStorage.setItem(key, JSON.stringify(v)));
35  return 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[] => {
47  const sel = selection.get();
48  return sel?.trackId === trackId ? sel.ids : [];
49};
50
51project.subscribe((p, old) => {
52  if (p.id === old.id) return;
53  cursor.set(0);
54  selection.set(null);
55  chordEdit.set(null);
56  activeTrackId.set(p.tracks[0]?.id ?? null);
57});