Files
Recorder-Tabber/src/components/fingering-diagram.tsx
T
Cursor Agentandchen 59d65333e3 Add a soprano recorder tab converter as a Linux AppImage.
The app turns a lead line into Baroque fingering tabs with rhythm kept, ships Twinkle and Ode to Joy, and reads photos locally with oemer when the bundled Python is present.

Co-authored-by: catpersonisme <catpersonisme@gmail.com>
2026-10-09 03:35:16 +00:00

136 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
fingeringMarks,
fingeringProse,
type Fingering,
} from "@/lib/fingering";
type HoleState = Fingering["holes"][number];
function Hole({
cx,
cy,
r,
state,
}: {
cx: number;
cy: number;
r: number;
state: HoleState;
}) {
return (
<g>
<circle cx={cx} cy={cy} r={r} fill="#fbf6ec" />
{state === "closed" ? <circle cx={cx} cy={cy} r={r - 1.1} fill="#1c3830" /> : null}
{state === "half" ? (
<path
d={`M ${cx} ${cy - r + 1} A ${r - 1} ${r - 1} 0 0 0 ${cx} ${cy + r - 1} Z`}
fill="#1c3830"
/>
) : null}
<circle cx={cx} cy={cy} r={r} fill="none" stroke="#5c3d28" strokeWidth="1.6" />
</g>
);
}
export function FingeringDiagram({
fingering,
pitch,
size = "lg",
}: {
fingering: Fingering | null;
pitch: string;
size?: "lg" | "sm";
}) {
const width = size === "lg" ? 112 : 88;
if (!fingering) {
return (
<div
className="flex items-center justify-center rounded-2xl border border-dashed border-destructive/50 bg-destructive/5 px-2 text-center text-xs leading-snug text-destructive"
style={{ width, height: size === "lg" ? 220 : 172 }}
role="img"
aria-label={`${pitch} is outside the C5 to D7 chart. No fingering is shown.`}
>
Outside C5–D7. No fingering.
</div>
);
}
const [h1, h2, h3, h4, h5, h6a, h6b, h7a, h7b] = fingering.holes;
const label = `${pitch}. ${fingeringProse(fingering)}. ${fingeringMarks(fingering).join(", ")}`;
return (
<svg
viewBox="0 0 104 228"
width={width}
role="img"
aria-label={label}
className="h-auto shrink-0"
>
<rect x="34" y="8" width="46" height="208" rx="20" fill="#e4cfae" stroke="#6a4328" strokeWidth="2" />
<path d="M46 28c8-16 22-16 30 0" fill="none" stroke="#6a4328" strokeWidth="2" />
<Hole cx={18} cy={40} r={8} state={fingering.thumb} />
<text x={14} y={62} textAnchor="middle" fontSize="8" fill="#6a4328">
T
</text>
<Hole cx={57} cy={58} r={8} state={h1} />
<Hole cx={57} cy={80} r={8} state={h2} />
<Hole cx={57} cy={102} r={8} state={h3} />
<Hole cx={57} cy={132} r={8} state={h4} />
<Hole cx={57} cy={154} r={8} state={h5} />
<Hole cx={48} cy={178} r={6.5} state={h6a} />
<Hole cx={66} cy={178} r={6.5} state={h6b} />
<Hole cx={48} cy={198} r={6.5} state={h7a} />
<Hole cx={66} cy={198} r={6.5} state={h7b} />
<text x={84} y={61} fontSize="8" fill="#7a5a3c">1</text>
<text x={84} y={83} fontSize="8" fill="#7a5a3c">2</text>
<text x={84} y={105} fontSize="8" fill="#7a5a3c">3</text>
<text x={84} y={135} fontSize="8" fill="#7a5a3c">4</text>
<text x={84} y={157} fontSize="8" fill="#7a5a3c">5</text>
<text x={84} y={181} fontSize="8" fill="#7a5a3c">6</text>
<text x={84} y={201} fontSize="8" fill="#7a5a3c">7</text>
{fingering.bell ? (
<text x={57} y={222} textAnchor="middle" fontSize="8" fill="#6a4328">
bell
</text>
) : null}
</svg>
);
}
export function RestMark({ label }: { label: string }) {
const kind = label.includes("whole")
? "whole"
: label.includes("half")
? "half"
: label.includes("sixteenth")
? "sixteenth"
: label.includes("eighth")
? "eighth"
: "quarter";
return (
<svg viewBox="0 0 64 120" width={72} className="h-auto" role="img" aria-label={`${label} rest`}>
<line x1="8" y1="60" x2="56" y2="60" stroke="#c4b39a" strokeWidth="2" />
{kind === "whole" ? <rect x="20" y="36" width="24" height="14" rx="1" fill="#2a2118" /> : null}
{kind === "half" ? <rect x="20" y="60" width="24" height="14" rx="1" fill="#2a2118" /> : null}
{kind === "quarter" ? (
<path
d="M34 28c8 8 8 14 0 20s-8 12 2 20 10 10 4 18"
fill="none"
stroke="#2a2118"
strokeWidth="4"
strokeLinecap="round"
/>
) : null}
{kind === "eighth" || kind === "sixteenth" ? (
<g fill="#2a2118">
<circle cx="36" cy="78" r="6" />
<path d="M36 72c10-14 16-20 8-36" fill="none" stroke="#2a2118" strokeWidth="3" />
{kind === "sixteenth" ? (
<path d="M32 64c10-10 14-14 6-26" fill="none" stroke="#2a2118" strokeWidth="3" />
) : null}
</g>
) : null}
</svg>
);
}