Show length on a staff under each fingering chart.
AppImage release / appimage (push) Successful in 50s

Dragging no longer selects text. The line box resizes, rests and spaces can be added, and Save PNG writes the row. Each chart keeps the foot at the top, and the treble staff shows the pitch with a notehead, stem, flags, and dots. Version 0.2.1.

Co-authored-by: catpersonisme <catpersonisme@gmail.com>
This commit is contained in:
Cursor Agentandchen committed 2026-10-11 09:49:17 +00:00
1 parent b354ea9f88
commit dcca7f5e83
11 files changed
+997 -177

No files matched your search

+41 -1
View File
@@ -1,17 +1,48 @@
import { DURATION_PRESETS, type DurationPreset } from "@/lib/duration";
import { newId } from "@/lib/score";
export type LineKind = "note" | "rest" | "space";
export type LineNote = {
id: string;
kind: LineKind;
pitch: string;
quarters: number;
label: string;
};
export const LINE_BOX = {
minWidth: 280,
minHeight: 200,
maxHeight: 880,
};
const QUARTER = DURATION_PRESETS.find((preset) => preset.quarters === 1) ?? DURATION_PRESETS[3];
export function createNote(pitch: string, preset: DurationPreset = QUARTER, id = newId("n")): LineNote {
return { id, pitch, quarters: preset.quarters, label: preset.label };
return { id, kind: "note", pitch, quarters: preset.quarters, label: preset.label };
}
export function createRest(preset: DurationPreset = QUARTER, id = newId("r")): LineNote {
return { id, kind: "rest", pitch: "", quarters: preset.quarters, label: preset.label };
}
export function createSpace(preset: DurationPreset = QUARTER, id = newId("s")): LineNote {
return { id, kind: "space", pitch: "", quarters: preset.quarters, label: preset.label };
}
/** Keep a resized line box on the page. `maxWidth` is the room the page actually has. */
export function clampLineBox(
width: number,
height: number,
maxWidth: number,
): { width: number; height: number } {
const limit = Math.max(160, maxWidth);
const minWidth = Math.min(LINE_BOX.minWidth, limit);
return {
width: Math.round(Math.min(limit, Math.max(minWidth, width))),
height: Math.round(Math.min(LINE_BOX.maxHeight, Math.max(LINE_BOX.minHeight, height))),
};
}
export function insertNote(notes: LineNote[], index: number, note: LineNote): LineNote[] {
@@ -36,6 +67,15 @@ export function removeNote(notes: LineNote[], id: string): LineNote[] {
return notes.filter((note) => note.id !== id);
}
const DURATION_ORDER = [...DURATION_PRESETS].sort((a, b) => a.quarters - b.quarters);
/** The next longer or shorter preset. Null at either end of the list. */
export function neighborDuration(quarters: number, direction: -1 | 1): DurationPreset | null {
const index = DURATION_ORDER.findIndex((preset) => Math.abs(preset.quarters - quarters) < 0.001);
if (index < 0) return null;
return DURATION_ORDER[index + direction] ?? null;
}
export function setDuration(notes: LineNote[], id: string, preset: DurationPreset): LineNote[] {
return notes.map((note) =>
note.id === id ? { ...note, quarters: preset.quarters, label: preset.label } : note,