AppImage release / appimage (push) Successful in 55s
The spiral sits on the G line, with the hook under the staff. The chart is a little wider so the holes stay the same size. Version 0.2.5. Co-authored-by: catpersonisme <catpersonisme@gmail.com>
68 lines
2.4 KiB
TypeScript
68 lines
2.4 KiB
TypeScript
import type { LineNote } from "@/lib/line";
|
|
import { chartOuterWidth, chartPixelHeight } from "@/lib/notation";
|
|
|
|
export const PNG_CHART_WIDTH = 88;
|
|
const GAP = 18;
|
|
const PAD = 28;
|
|
|
|
export { chartPixelHeight };
|
|
|
|
/** A quarter space is about one chart wide. Longer spaces take more room. */
|
|
export function spacePixelWidth(quarters: number, chartWidth: number): number {
|
|
return Math.max(Math.round(chartWidth * 0.45), Math.round(chartWidth * Math.max(quarters, 0)));
|
|
}
|
|
|
|
export type PngItem = {
|
|
kind: LineNote["kind"];
|
|
caption: string;
|
|
glyphWidth: number;
|
|
glyphHeight: number;
|
|
};
|
|
|
|
export function pngItemFromNote(note: Pick<LineNote, "kind" | "pitch" | "quarters" | "label">, chartWidth = PNG_CHART_WIDTH): PngItem {
|
|
const drawn = chartOuterWidth(chartWidth);
|
|
const glyphHeight = chartPixelHeight(chartWidth);
|
|
const caption = note.kind === "note" ? note.pitch : note.kind === "rest" ? "Rest" : "Space";
|
|
return {
|
|
kind: note.kind,
|
|
caption,
|
|
glyphWidth: note.kind === "space" ? spacePixelWidth(note.quarters, drawn) : drawn,
|
|
glyphHeight,
|
|
};
|
|
}
|
|
|
|
export function pngSlotWidth(item: PngItem): number {
|
|
const text = item.caption.length * 8;
|
|
return Math.max(item.glyphWidth, text, 48);
|
|
}
|
|
|
|
export const PNG_ROW_GAP = 36;
|
|
|
|
/** One header, then every fingering line stacked underneath. */
|
|
export function pngStackLayout(rows: PngItem[][], title: string) {
|
|
const laid = rows.map((items) => pngLayout(items, title));
|
|
const width = Math.max(1, ...laid.map((row) => row.width));
|
|
const blocks = laid.reduce((sum, row) => sum + row.glyphHeight + 28, 0);
|
|
const height = PAD + 44 + blocks + PNG_ROW_GAP * Math.max(0, laid.length - 1) + PAD;
|
|
return { width, height, rows: laid, pad: PAD, rowGap: PNG_ROW_GAP };
|
|
}
|
|
|
|
export function pngLayout(items: PngItem[], title: string) {
|
|
const slots = items.map((item) => ({ item, width: pngSlotWidth(item) }));
|
|
const content =
|
|
slots.reduce((sum, slot) => sum + slot.width, 0) + GAP * Math.max(0, slots.length - 1);
|
|
const titleWidth = Math.max(220, title.length * 13 + 8);
|
|
const width = Math.max(titleWidth, content, 1) + PAD * 2;
|
|
const glyphHeight = chartPixelHeight(PNG_CHART_WIDTH);
|
|
const height = PAD + 44 + glyphHeight + 28 + PAD;
|
|
return { width, height, slots, glyphHeight, pad: PAD, gap: GAP };
|
|
}
|
|
|
|
export function pngFilename(title: string): string {
|
|
const slug = title
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-|-$/g, "");
|
|
return `${slug || "line"}.png`;
|
|
}
|