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, 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[] { const next = notes.slice(); const at = Math.max(0, Math.min(index, next.length)); next.splice(at, 0, note); return next; } /** `toIndex` is the slot in the current list, counting the note that is moving. */ export function moveNote(notes: LineNote[], id: string, toIndex: number): LineNote[] { const from = notes.findIndex((note) => note.id === id); if (from < 0) return notes; const next = notes.filter((note) => note.id !== id); const adjusted = toIndex > from ? toIndex - 1 : toIndex; const at = Math.max(0, Math.min(adjusted, next.length)); next.splice(at, 0, notes[from]); return next; } 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, ); } export function cloneLine(notes: LineNote[]): LineNote[] { return notes.map((note) => ({ ...note, id: newId("n") })); } /** -1 when the pointer is outside the line. Otherwise the insertion slot from left to right. */ export function dropIndexFromPoint( pointerX: number, pointerY: number, line: { left: number; right: number; top: number; bottom: number }, noteCenters: number[], slack = 36, ): number { const inside = pointerX >= line.left - slack && pointerX <= line.right + slack && pointerY >= line.top - slack && pointerY <= line.bottom + slack; if (!inside) return -1; for (let index = 0; index < noteCenters.length; index += 1) { if (pointerX < noteCenters[index]) return index; } return noteCenters.length; } function preset(quarters: number, label: string): DurationPreset { return { quarters, label }; } /** Two short lines to edit. They are not a full tune. */ export const LINE_EXAMPLES = [ { id: "twinkle", title: "Twinkle, opening", blurb: "Seven notes. The last one is held.", notes: [ createNote("C5", preset(1, "quarter"), "twinkle-1"), createNote("C5", preset(1, "quarter"), "twinkle-2"), createNote("G5", preset(1, "quarter"), "twinkle-3"), createNote("G5", preset(1, "quarter"), "twinkle-4"), createNote("A5", preset(1, "quarter"), "twinkle-5"), createNote("A5", preset(1, "quarter"), "twinkle-6"), createNote("G5", preset(2, "half"), "twinkle-7"), ], }, { id: "ode", title: "Ode, held note", blurb: "A dotted quarter, an eighth, and a half note.", notes: [ createNote("E5", preset(1.5, "dotted quarter"), "ode-1"), createNote("D5", preset(0.5, "eighth"), "ode-2"), createNote("D5", preset(2, "half"), "ode-3"), ], }, ] as const;