[번역] CSS Grid로 악보 그리기

Saetbyeol·6일 전
18

translations.zip

목록 보기
16/16
post-thumbnail

원문 <Printing music with CSS Grid>를 저자(@stephband)의 허락을 받아 한국어로 번역한 글입니다. 🙂

⚠️ 원문에서는 실제 저자가 개발한 악보 렌더러로 구현한 보표 및 악보를 예시로 사용합니다. 하지만 블로그 플랫폼 특성상 해당 코드를 온전히 구현하기가 쉽지 않아 스크린샷과 원문 링크로 대체합니다. 실제 결과물이 궁금하시다면, 원문을 참고하시길 바랍니다.

공연의 클라이맥스에서 연주자가 땀을 뻘뻘 흘리며 조그만 모바일 화면에서 A4 크기의 PDF를 확대하는 모습을 자주 봤습니다. 우리는 유연하고 반응성이 좋은 악보를 그리는 웹이 필요합니다!

웹에서 악보 표기법은 텍스트처럼 접근하기 쉽고 유동적이어야 하는데, 아직은 그렇지 못하다는 현실에 안타까운 감정이 들었습니다. 그래서 이 시급한 문제를 해결해 보고자 했습니다.

Scribe 프로토타입

몇 년 전, 저는 JSON에서 SVG를 출력하는 Scribe라는 악보 렌더러의 프로토타입을 만들었습니다. 원래 목표는 반응형 음악 렌더러를 만드는 것이었고 프로토타입은 꽤 훌륭했습니다. 다만 더 진행하려면 복잡한 다중 패스 레이아웃 엔진을 작성해야 했고, 다른 일들이 많아 해당 작업은 중단되었습니다.

Scribe를 만든 직후, 저는 Cruncher에서 프로젝트에 Grid를 적용하느라 바빴습니다. 그 과정에서 낯익은 느낌이 들었고, 이것이 Scribe의 레이아웃 문제에 대한 해답이 될 것 같다는 생각이 들었습니다.

Scribe 0.2로 렌더링한 SVG

.stave 클래스

역자주: 이 글에서 사용된 단어 'stave'를 오선보와 음자리표 등을 모두 포함하는 '보표'로 번역합니다.

보표는 그리드와 유사합니다. 음정은 세로축에 표시되며, 시간은 가로축을 따라 왼쪽에서 오른쪽으로 흐릅니다. 이 두 축을 두 개의 별도 클래스로 정의합니다. 그리드 행을 정의하는 세로축은 .stave라고 합니다. 시간 축에 대해서는 잠시 후에 설명하겠습니다.

.stave에는 고정 크기의 그리드 행과 배경 이미지를 가지며, 각 행의 이름은 표준 음이름과 같습니다. 따라서 높은 음자리표의 경우 행 맵은 다음과 같습니다.

.stave {
    display: grid;
    row-gap: 0;
    grid-template-rows:
        [A5] 0.25em [G5] 0.25em [F5] 0.25em [E5] 0.25em
        [D5] 0.25em [C5] 0.25em [B4] 0.25em [A4] 0.25em
        [G4] 0.25em [F4] 0.25em [E4] 0.25em [D4] 0.25em
        [C4] 0.25em ;

    background-image:    url('/path/to/stave.svg');
    background-repeat:   no-repeat;
    background-size:     100% 2.25em;
    background-position: 0 50%;
}

이를 <div> 태그에 적용하면 다음과 같이 보입니다.

오선보만 있는 보표

좋습니다. 많은 기능을 구현한 건 아니지만, 각 줄과 각 칸에 각 행을 식별할 수 있는 고유한 음이름이 지정된 그리드 선이 있음을 확인할 수 있습니다.

명명된 그리드 행

보표 위에 음 배치하기

보표의 행은 여러 음을 포함할 수 있습니다. 예를 들어 G♭, G, G♯ 음정은 모두 G 보표 줄에 있어야 합니다.

이러한 음의 DOM 요소를 올바른 행에 배치하기 위해, data-pitch 속성에 음이름을 넣고 CSS를 사용하여 data-pitch 값을 보표 행에 매핑하겠습니다.

.stave > [data-pitch^="G"][data-pitch$="4"] { grid-row-start: G4; }

위 선택자는 'G'로 시작하고 '4'로 끝나는 음을 특정하므로 'G♭4', 'G4', 'G♯4'(더블플랫 'G𝄫4', 더블샵 'G𝄪4' 포함)를 G4 행에 할당합니다. 모든 보표 행에 대해 이 작업을 수행해야 합니다.

.stave > [data-pitch^="A"][data-pitch$="5"] { grid-row-start: A5; }
.stave > [data-pitch^="G"][data-pitch$="5"] { grid-row-start: G5; }
.stave > [data-pitch^="F"][data-pitch$="5"] { grid-row-start: F5; }
.stave > [data-pitch^="E"][data-pitch$="5"] { grid-row-start: E5; }
.stave > [data-pitch^="D"][data-pitch$="5"] { grid-row-start: D5; }

...

.stave > [data-pitch^="D"][data-pitch$="4"] { grid-row-start: D4; }
.stave > [data-pitch^="C"][data-pitch$="4"] { grid-row-start: C4; }

이 정도면 보표에 기호를 배치할 만하겠군요! Scribe 프로토타입을 위해 준비한 SVG 기호가 많으니 몇 개를 보표에 추가해 보겠습니다.

<div class="stave">
    <svg data-pitch="G4" class="head">
        <use href="#head[2]"></use>
    </svg>
    <svg data-pitch="E5" class="head">
        <use href="#head[2]"></use>
    </svg>
</div>

G4, E5 두 음이 그려진 보표

착착 진행되고 있네요. 다음으로 넘어가볼까요?

.bar 클래스와 박자

리듬은 다루기가 좀 더 까다로울 수 있습니다. 모든 종류의 리듬을 지원하는 최소 리듬분할은 딱 하나로 정해지지 않습니다. 그리드 안에서 어떤 최소 음표 길이와 어떤 교차 리듬을 지원할지 판단해야 합니다.

한 박자 당 24열 접근 방식은 8분음표(12열), 16분음표(6열), 32분음표(3열)뿐만 아니라 그 음표들의 셋잇단음도 균일하게 배치할 수 있습니다. 이는 좋은 기준점이 됩니다.

4박자 마디(bar)가 4 × 24 = 96개의 그리드 열로 정의되며, 맨 앞과 맨 끝에 열이 추가됩니다.

.bar {
    column-gap: 0.03125em;
    grid-template-columns:
        [bar-begin]
        max-content
        repeat(96, minmax(max-content, auto))
        max-content
        [bar-end];
}

마디 앞뒤에 ::before::after로 몇 개의 마디를 추가하고, data-pitch="B4"로 설정된 음자리표 기호를 가운데 정렬된 상태로 악보에 두면 다음과 같습니다.

<div class="stave bar">
    <svg data-pitch="B4" class="treble-clef">
        <use href="#treble-clef"></use>
    </svg>
</div>

높은 음자리표가 있는 보표

자세히 살펴보면 음표가 첫 번째 열로 들어갑니다. 또한 박자당 작은 column-gap으로 구분된 너비가 0인 24개의 열이 있으며, 4박자 마디이므로 총 96개의 열이 존재하는 걸 볼 수 있습니다.

그리드 열

박자에 기호 위치시키기

이번에는 data-beat 속성을 사용하여 요소에 박자를 할당하고 CSS 규칙을 사용하여 각 박자를 그리드 열에 매핑하겠습니다. CSS 맵은 다음과 같으며, 각 박자의 1/24마다 다음의 규칙을 갖습니다.

.bar > [data-beat^="1"]    { grid-column-start: 2; }
.bar > [data-beat^="1.04"] { grid-column-start: 3; }
.bar > [data-beat^="1.08"] { grid-column-start: 4; }
.bar > [data-beat^="1.12"] { grid-column-start: 5; }
.bar > [data-beat^="1.16"] { grid-column-start: 6; }
.bar > [data-beat^="1.20"] { grid-column-start: 7; }
.bar > [data-beat^="1.25"] { grid-column-start: 8; }

...

.bar > [data-beat^="4.95"] { grid-column-start: 97; }

속성을 ^= starts-with 선택자로 사용하면 규칙이 오류를 허용합니다. 결국 어느 지점에서는 반올림되지 않은 숫자나 부동 소수점 숫자가 data-beat으로 렌더링 됩니다. 소수점 이하 두 자리라면 1/24박자 그리드 열을 식별하는 데 충분합니다.

이를 stave 클래스와 함께 사용하여 기호를 박자와 음높이에 따라 위치시킬 수 있습니다. 이를 위해 data-beat15사이로 설정하고, data-pitch를 음표 이름으로 설정합니다. 이 과정에서 해당 기호를 포함하는 박자 열이 확장됩니다.

<div class="stave bar">
    <svg class="clef" data-pitch="B4"></svg>
    <svg class="flat" data-beat="1" data-pitch="Bb4"></svg>
    <svg class="head" data-beat="1" data-pitch="Bb4"></svg>
    <svg class="head" data-beat="2" data-pitch="D4"></svg>
    <svg class="head" data-beat="3" data-pitch="G5"></svg>
    <svg class="rest" data-beat="4" data-pitch="B4"></svg>
</div>

높은 음자리표, 시플랫, 레, 높은 솔, 4분쉼표 순으로 그려진 보표지만 각 음표의 꼬리가 없다. 모든 음표는 4분음표이다.

앗, 음표에 줄기가 없군요?

바로 위 보표에 음표의 줄기가 있다.

오! 꼬리도 추가해 볼까요?

위 보표에서 , 높은 솔이 8분음표로 변경되고 각 음표 사이에 8분 쉼표가 추가되었다

됐습니다. 꼬리 간격은 CSS margin으로 보기 좋게 개선할 수 있지만 어쨌든 위치는 잘 지정되었군요.

유동적이고 반응성이 좋은 표기법

아래와 같이 여러 개의 마디를 플렉스박스(flexbox) 컨테이너에 넣고 래핑하면 반응형 악보를 만들 수 있습니다.

<figure class="flex">
    <div class="treble-stave stave bar"></div>
    <div class="treble-stave stave bar"></div>
    <div class="treble-stave stave bar"></div></figure>

첫 번째 마디에는 높은 음자리표, 레, 파샵, 라, 높은도샵에 해당하는 4분음표 4개. 두 번째 마디에는 시, 라, 파샵, 레에 해당하는 4분음표 4개. 세 번째 마디에는 미, 솔, 라, 높은 레에 해당하는 4분음표 4개. 네 번째 마디에는 높은도샵, 4분쉼표, 2분쉼표. 그 다음 줄에 있는 첫 번째 마디에 레, 파샵, 라, 높은도샵 4분음표 4개. 두 번째 마디에 시, 라, 파샵, 레 4분음표 4개.

아직 부족한 점이 많지만 시작하기에 좋은 기반이 마련되어 있습니다. 기존 온라인 악보 렌더러보다 더 우아하게 래핑합니다.

음표 사이의 간격

음표 꼬리의 연결줄(beams)을 잠시 무시하고 보면, 시간적으로 가까운 음표 머리들이 약간 더 가깝게 렌더링 되는 게 보입니다.

높은 솔 4분음표 2개, 높은 솔 8분음표 2개가 빔으로 연결되어 있다. 높은 솔 16분음표 4개가 빔으로 연결되어 있다. 빔으로 연결된 경우 각 음표 사이가 더 가깝다.

이는 작은 column-gap이 만들어내는 섬세하고 의도적인 효과입니다. 이 간격은 기호 요소에서 일종의 시간적 '에테르' 역할을 합니다. 열 자체는 음표 머리가 없는 경우에는 너비가 0이지만, 박자 간격이 더 먼 이벤트 사이에는 더 큰 열 간격(한 박자당 24개씩)이 있으므로 더 멀게 렌더링 되는 것입니다.

기호의 margin을 조정하여 일정한 간격을 두도록 할 수도 있습니다. 일정한 간격을 얻기 위해 column-gap을 줄이고 음표 머리의 margin을 늘리는 것이죠.

높은 솔 4분음표 2개, 높은 솔 8분음표 2개가 빔으로 연결되어 있다. 높은 솔 16분음표 4개가 빔으로 연결되어 있다. 모든 음표 사이의 거리가 동일하다.

하지만 일정한 음표 머리 간격으로 인해 오히려 독자는 리듬이 얼마나 빠른지 알 수 없으므로 좋지 않습니다. 제 말의 요점은 CSS가 악보의 레이아웃에 대한 훌륭한 제어 기능을 제공한다는 것입니다. 지금의 목표는 가독성을 위해 크기나 간격과 같은 값들을 조정하는 것입니다.

음자리표와 박자표

왜 수직과 수평 간격을 분리하여 별도의 클래스를 사용했는지 궁금하신가요? 분리하면 한 축을 다른 축과 독립적으로 바꿀 수 있기 때문입니다. 다음 멜로디를 보시죠.

보표 첫 번재 마디에 높은 음자리표, 4분의4박자 박자표, 8분쉼표, 낮은시 점4분음표, 레 4분음표, 파샵4분음표가 있다. 두 번째 마디에 미 4분음표, 레 4분음표, 빔으로 연결된 낮은 시와 낮은 솔 8분음표, 4분쉼표가 있다.

여기에서 높은 음자리표를 낮은 음자리표로만 바꾸고 싶다면, stave 클래스를 bass-stave 클래스로 교체하기만 하면 됩니다. bass-stave 클래스는 동일한 data-pitch 속성들을 낮은 음자리표의 보표에 맞게 추가합니다.

<div class="bass-stave bar">...</div>

보표 첫 번째 마디에 낮은 음자리표, 4분의4박자 박자표, 8분쉼표, 높은 솔 점4분음표, 높은 시 4분음표, 높은 레샵(F#4) 4분음표가 있다. 두 번째 마디에 높은 도(E4) 4분음표, 높은 시 4분음표, 빔으로 연결된 높은 솔과 높은 미 8분음표, 4분쉼표가 있다.

또는 .bar의 120개 grid-template-columnsdata-duration="5"를 매핑한 CSS를 사용하면 5/4박자인 동일한 보표를 그릴 수 있습니다.

<div class="bass-stave bar" data-duration="5">...</div>

보표 첫 번째 마디에 낮은 음자리표, 4분의5박자 박자표, 8분쉼표, 높은 솔 점4분음표, 높은 시 4분음표, 높은 레샵(F#4) 4분음표, 높은 도 4분음표가 있다. 두 번째 마디에 높은 시 4분음표, 빔으로 연결된 높은 솔과 높은 미 8분음표, 점4분쉼표가 있다.

당연히 모든 세부 사항을 말씀드리진 않았습니다. 모든 변경이 클래스 대체처럼 간단하지 않으며, 일부 음표 줄기와 덧줄을 다시 배치해야 할 수도 있습니다.

다음은 음정을 완전히 다시 매핑하는 보표 클래스입니다. 일반 악기 디지털 인터페이스에서 드럼과 퍼커션 음성들은 키보드의 하단 옥타브에 여러 음으로 배치되어 있지만, 이러한 음표의 위치는 의미가 없습니다. drums-stave CSS 클래스를 정의하여 이러한 음들을 올바른 행에 매핑할 수 있습니다.

<div class="drums-stave bar" data-duration="4">...</div>

4분의4박자 두 마디 보표 위에 여러 음표와 드럼을 나타내는 노트가 섞여 있다.

<div class="percussion-stave bar" data-duration="4">...</div>

4분의4박자 두 마디 위에 드럼 표기법으로 나타낸 음표와 쉼표가 있다.

아주 읽기 쉬운 드럼 표기법이군요. 아주 만족스럽네요.

화음과 가사

CSS 그리드를 사용하면 표기법 그리드 안의 다른 기호도 정렬할 수 있습니다. 화음과 가사, 강약 등을 시간 흐름에 맞춰 지정할 수 있습니다.

4분의4박자 네 마디에 해당하는 음표들이 있고 보표 상단에 코드가 적혀 있다. 보표 사이에는 가사가 적혀 있다. 가사는 'In the bleak midwinter, frosty wind made moan'이다.

그렇다면 음표의 연결줄은?

연결줄, 화음 및 일부 긴 쉼표들은 data-duration 속성을 grid-column-end의 span 값에 매핑하여 여러 열을 걸치도록 합니다.

.stave > [data-duration="0.25"] { grid-column-end: span 6; }
.stave > [data-duration="0.5"]  { grid-column-end: span 12; }
.stave > [data-duration="0.75"] { grid-column-end: span 18; }
.stave > [data-duration="1"]    { grid-column-end: span 24; }
.stave > [data-duration="1.25"] { grid-column-end: span 30; }
...

크기 조정

마지막으로 em 단위가 전체 시스템의 크기의 기반이므로, 크기를 조정하려면 font-size를 변경하기만 하면 됩니다.

4분의2박자 보표 위에 두 마디가 있다.

플렉스와 그리드의 한계

완벽해 보이시나요? 솔직히 너무 잘 작동해서 놀랍긴 하지만, 아쉬운 점을 찾아보자면... 1. CSS는 래핑된 각 줄의 시작 부분에 새 음자리표/조표를 자동으로 배치하지 못합니다. 2. 새 줄의 새 음표 머리에 이전 줄에 있는 음표의 머리를 묶을 수 없습니다. 3. 1/16음표와 1/32음표는 그리드에 배치하기 전까지는 음표 줄기의 위치를 정확히 알 수 없기 때문에 연결선을 정렬하기 어렵습니다.

오선보 위에 빔으로 연결된 6개의 음표들, 4분음표, 4분쉼표가 있다. 빔의 위치가 맞지 않아 어색해 보이는 보표이다.

따라서 작업을 완전히 끝내려면 자바스크립트를 좀 더 정리해야 합니다. 하지만 레이아웃 작업의 대부분을 CSS로 처리하므로 자바스크립트로 하는 레이아웃 작업이 훨씬 줄어듭니다.

여러분의 생각은 어떠신가요?

이 CSS 시스템이나 게시물이 마음에 드시거나 개선할 점이 있으면 저에게 알려주세요. 제 소셜 미디어 계정은 블루스카이 @stephen.band, 마스토돈 @stephband@front-end.social, (아직은) 트위터 @stepband입니다. 또는 Scribe 저장소에서 같이 만들 수도 있어요.

<scribe-music> 악보를 렌더링하기 위한 커스텀 요소

Scribe 코드 저장소 https://github.com/stephband/scribe/
Scribe 데이터 포맷 https://github.com/soundio/sequence-json

저는 이 새로운 CSS 시스템을 중심으로 인터프리터를 작성하고 이를 <scribe-music> 요소에 포함시켰습니다. 아직 프로덕션에 사용할 수 있는 수준은 아니지만 반응형 리드 시트를 렌더링 하고 드럼 표기법을 작성할 수 있으므로 흥미롭고 유용하다고 생각합니다.

어떻게 그려볼까요?

<scribe-music> 요소는 내부 콘텐츠에 있는 데이터로 악보를 렌더링합니다.

<scribe-music type="sequence">
    0 chord D maj 4
    0 F#5 0.2 4
    0 A4  0.2 4
    0 D4  0.2 4
</scribe-music>

D메이저코드를 나타내는 음 3개와 높은 음자리표가 그려져 있다.

또는 src 속성의 파일도 가능합니다. 아래 JSON 파일처럼 말이죠.

<scribe-music
    clef="drums"
    type="application/json"
    src="/static/blog/printing-music/data/caravan.json">
</scribe-music>

여러 악기의 표기법이 혼합되어 있는 두 마디가 있다.

요소의 .data 프로퍼티의 자바스크립트 객체로도 가능합니다.

모든 기능의 기본 문서는 README에서 확인해 보세요.

한번 써보세요

여러분의 웹에서 다음과 같이 리소스를 가져와 현재 개발 단계의 빌드를 사용할 수 있습니다.

<link rel="stylesheet" href="https://stephen.band/scribe/scribe-music/module.css" />
<script type="module" src="https://stephen.band/scribe/scribe-music/module.js"></script>

위에서도 말씀드렸듯이 아직 개발 단계입니다. 자동 스펠러 조정, 1/16음표 연결줄 수정, 잇단음표 감지 및 표시 등 Scribe 0.3에서 바로 개선할 수 있는 몇 가지 기능 외에도 장기적으로 검토하고 싶은 몇 가지 기능은 다음과 같습니다.

  • SMuFL 폰트 지원 - 기호에 사용되는 폰트를 변경하는 기능으로 지금까지는 확장 문자 집합을 브라우저 간에 안정적으로 표시할 수 없었음
  • 중첩된 시퀀스 지원 - 여러 파트의 곡을 사용할 수 있도록 함
  • 분할된 보표 렌더링 - 하나의 보표에 여러 파트를 배치하는 기능으로, 현재 드럼 보표와 피아노 보표를 음정별로 자동 분할되므로 해당 메커니즘은 이미 절반 완성되었음
  • 다중 보표 렌더링 - 여러 파트를 여러 정렬된 보표에 배치함

<scribe-music>로 렌더링했으며 조옮김이 가능한 Dolphin Dance 시트로 이 글을 마칩니다.

Scribe-music으로 렌더링한 dolphin dance 전체 악보가 있다. 조옮김이 가능한 옵션 셀렉터가 있다.

2개의 댓글

comment-user-thumbnail
5일 전

좋네요.. 이거 응용해서 score-hub 같은 사이트를 만들어보고 싶어졌습니다.

1개의 답글