전체 화면의 악보 영역을 넓히고 렌더링된 악보 높이에 따라 배율을 자동으로 맞춘다. 모바일 가로 화면에서는 최소 높이를 줄여 다른 연습 영역을 지나치게 가리지 않도록 했다. 검사: 프런트 타입 검사, 전체 70개 테스트, Docker 운영 빌드, 데스크톱·태블릿·모바일 가로 화면 확인 운영 반영: 완료 백업: /home/syskiller/piano/backups/bach-to-basics/20261003-1730-sheet-fit
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { fitHorizontalSheetScale } from "../components/SheetMusic/SheetMusicView";
|
||||
|
||||
describe("fitHorizontalSheetScale", () => {
|
||||
it("악보 높이가 영역 안에 들어오면 현재 배율을 유지한다", () => {
|
||||
expect(fitHorizontalSheetScale(0.62, 220, 240)).toBe(0.62);
|
||||
});
|
||||
|
||||
it("악보가 넘치면 아래 보표가 잘리지 않도록 배율을 낮춘다", () => {
|
||||
expect(fitHorizontalSheetScale(0.62, 300, 240)).toBeCloseTo(0.4795, 3);
|
||||
});
|
||||
|
||||
it("작은 화면에서도 최저 가독성 배율을 보장한다", () => {
|
||||
expect(fitHorizontalSheetScale(0.62, 600, 180)).toBe(0.42);
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,30 @@ function sanitizeMusicXml(xml: string): string {
|
||||
return xml.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/\bon\w+\s*=/gi, "data-removed=");
|
||||
}
|
||||
|
||||
const HORIZONTAL_SHEET_BASE_SCALE = 0.62;
|
||||
const HORIZONTAL_SHEET_MIN_SCALE = 0.42;
|
||||
|
||||
/**
|
||||
* 전체 화면의 악보는 곡마다 보표와 옥타브 표기의 높이가 달라진다.
|
||||
* 고정 배율만 사용하면 복잡한 양손 악보의 아래 보표가 영역 밖으로 잘릴 수 있다.
|
||||
* 실제 렌더링 높이가 가용 높이를 넘을 때만 배율을 낮추고 작은 화면에서도
|
||||
* 음표를 알아볼 수 있도록 최저 배율을 보장한다.
|
||||
*/
|
||||
export function fitHorizontalSheetScale(
|
||||
currentScale: number,
|
||||
renderedHeight: number,
|
||||
availableHeight: number,
|
||||
): number {
|
||||
if (renderedHeight <= 0 || availableHeight <= 0 || renderedHeight <= availableHeight) {
|
||||
return currentScale;
|
||||
}
|
||||
|
||||
const safetyMargin = 8;
|
||||
const usableHeight = Math.max(1, availableHeight - safetyMargin);
|
||||
const fitted = currentScale * (usableHeight / renderedHeight);
|
||||
return Math.max(HORIZONTAL_SHEET_MIN_SCALE, Math.min(currentScale, fitted));
|
||||
}
|
||||
|
||||
export function SheetMusicView() {
|
||||
const scrollRef = useRef<HTMLDivElement>(null); // outer overflow-auto container
|
||||
const containerRef = useRef<HTMLDivElement>(null); // inner AlphaTab host
|
||||
@@ -49,6 +73,7 @@ export function SheetMusicView() {
|
||||
// SheetMusicView mounts fresh per layout mode (not a singleton), so reading
|
||||
// the mode once at init is safe for this instance's lifetime.
|
||||
const horizontal = useAppStore.getState().settings.layoutMode === "all";
|
||||
let horizontalScale = HORIZONTAL_SHEET_BASE_SCALE;
|
||||
|
||||
const api = new AlphaTabApi(containerRef.current, {
|
||||
core: {
|
||||
@@ -70,7 +95,7 @@ export function SheetMusicView() {
|
||||
// Smaller scale in the horizontal "all" band so the full grand staff
|
||||
// (both clefs) fits the short band height without the bass clef being
|
||||
// clipped. Page mode keeps the larger, more readable 0.9.
|
||||
scale: horizontal ? 0.62 : 0.9,
|
||||
scale: horizontal ? horizontalScale : 0.9,
|
||||
// stretchForce 0 = notes keep their natural rhythmic proportions (like
|
||||
// a printed score). A very small value (0.1) avoids a ragged right
|
||||
// edge on the last system without visibly compressing the spacing.
|
||||
@@ -97,6 +122,34 @@ export function SheetMusicView() {
|
||||
api.masterVolume = 0;
|
||||
setApiReady(true);
|
||||
|
||||
// 곡마다 보표와 8va·8vb 표기의 세로 높이가 달라진다. 렌더링이 끝난
|
||||
// 뒤 실제 높이를 재서 악보 띠 안에 양손 보표가 모두 들어오도록 배율을
|
||||
// 한 번 더 맞춘다. 계산 결과가 같으면 다시 렌더링하지 않아 반복을 막는다.
|
||||
try {
|
||||
api.postRenderFinished.on(() => {
|
||||
if (!horizontal || disposed) return;
|
||||
requestAnimationFrame(() => {
|
||||
const scrollEl = scrollRef.current;
|
||||
const scoreEl = containerRef.current;
|
||||
if (!scrollEl || !scoreEl || disposed) return;
|
||||
|
||||
const nextScale = fitHorizontalSheetScale(
|
||||
horizontalScale,
|
||||
scoreEl.scrollHeight,
|
||||
scrollEl.clientHeight - 16,
|
||||
);
|
||||
if (horizontalScale - nextScale < 0.01) return;
|
||||
|
||||
horizontalScale = nextScale;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(api.settings as any).display.scale = horizontalScale;
|
||||
api.render();
|
||||
});
|
||||
});
|
||||
} catch {
|
||||
/* 이전 AlphaTab 버전에서는 렌더 완료 이벤트가 없을 수 있다. */
|
||||
}
|
||||
|
||||
// AlphaTab의 파일 해석과 렌더링 오류는 비동기로 전달된다. 빈 화면만
|
||||
// 남기지 않고 사용자가 원인을 알 수 있도록 화면에 오류를 표시한다.
|
||||
try {
|
||||
|
||||
@@ -356,7 +356,7 @@ export function PracticeView() {
|
||||
<div
|
||||
key="sheet-band"
|
||||
className="flex flex-col min-h-0 overflow-hidden"
|
||||
style={{ flex: "0 0 32%", minHeight: 130 }}
|
||||
style={{ flex: "0 0 38%", minHeight: "clamp(150px, 26vh, 220px)" }}
|
||||
>
|
||||
<SheetMusicView />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user