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>
136 lines
4.3 KiB
TypeScript
136 lines
4.3 KiB
TypeScript
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>
|
||
);
|
||
}
|