DevGang
Авторизоваться

Печать музыки с помощью CSS Grid

Слишком часто я был свидетелем того, как музыкант-импровизатор с потными руками пытался масштабировать PDF-файл формата А4 на крошечном экране мобильного телефона в разгар концерта. Нам нужен плавный и отзывчивый рендеринг музыки для Интернета!

Нотная запись должна быть такой же доступной и гибкой, как и текст в Интернете; то, что это еще не так, является чем-то вроде оскорбления для моих чувств. Давайте решим эту насущную проблему.

Прототип Писца

Несколько лет назад я создал прототип музыкального рендерера Scribe, который выводит SVG из JSON. Первоначальной целью было создание адаптивного музыкального рендерера. Это была хорошая демонстрация, но для прогресса мне пришлось написать сложный механизм многопроходной компоновки, да и другие вещи мешали.

Вскоре после этого я был занят внедрением Grid в наши проекты в Cruncher, когда что-то в нем показалось мне знакомым, и я задался вопросом, не может ли это быть ответом на некоторые проблемы с макетом, которые я решал в Scribe.

Класс .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, представляющие эти высоты тона, в их правильных строках, я собираюсь поместить имена питчей в атрибуты тона данных и использовать CSS для сопоставления значений тона данных со строками нотоносцев.

.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; }

Этого должно быть достаточно, чтобы начать размещать символы на нотном стане! У меня есть несколько SVG-символов, подготовленных для прототипа Scribe, поэтому давайте попробуем разместить парочку на нотоносце:

<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>

Это выглядит многообещающе. В следующий раз.

Класс .bar и его удары

С ритмом, пожалуй, немного сложнее справиться. Не существует одного очевидного наименьшего ритмического разделения, которое можно было бы принять, которое поддерживало бы все виды ритмов. Необходимо принять решение о том, какую минимальную длину нот и какие перекрестные ритмы поддерживать внутри сетки.

Подход с 24 столбцами на долю поддерживает разделение долей для равномерного расположения восьмых нот (12 столбцов), шестнадцатых нот (6 столбцов), 32-х нот (3 столбца), а также значений триолей этих нот. Это хорошая отправная точка.

Вот полоса из 4 долей, определяемая как 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>

Посмотрите на это, и мы увидим, что ключ попал в первый столбец, и имеется 96 столбцов нулевой ширины, по 24 на долю, каждый из которых разделен небольшим промежутком между столбцами:

Размещение символов в долях

На этот раз я собираюсь использовать атрибуты 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; }

Атрибут ^= селектор начинается с делает правило устойчивым к ошибкам. В какой-то момент неокругленные числа или числа с плавающей запятой неизбежно будут преобразованы в бит данных. Двух десятичных знаков достаточно, чтобы определить столбец сетки с размером 1/24 доли.

Объединив это с нашим классом нотоносцев, мы сможем позиционировать символы по доле и высоте, установив data-beat на долю от 1 до 5, а 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>

Ооо. Стебли?

Ага. Решка?

Ага. Расстояние между хвостами можно улучшить (что должно быть достигнуто с запасом), но позиционирование работает.

Гибкие и отзывчивые обозначения

Соберите целую кучу подобных тактов вместе в контейнере 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>

Здесь явно не хватает множества вещей, но это отличная база для начала. Он уже обертывается более изящно, чем я когда-либо видел в онлайн-рендерерах музыки.

Пространство между нотами

Не обращая внимания на эти лучи, обратите внимание, что ноты, которые встречаются ближе друг к другу по времени, визуализируются немного ближе друг к другу:

Это тонкий, преднамеренный эффект, создаваемый небольшим column-gap, который служит своего рода временным 'ether', в который вставляются элементы символов. Сами столбцы имеют нулевую ширину, если в них нет заголовка ноты, но между событиями, которые находятся дальше друг от друга по долям, имеется больше промежутков в столбцах - 24 на долю, и, следовательно, большее расстояние.

Постоянным интервалом можно управлять, регулируя поля символов. Чтобы добиться здесь более постоянного интервала, мы бы уменьшили интервал между столбцами, увеличив при этом поля заголовков примечаний:

Но, это выглядит плохо, потому что расстояние между головами не дает читателю представления о том, насколько быстрый ритм. Дело в том, что CSS дает нам хороший контроль над метриками. И теперь цель состоит в том, чтобы настроить эти показатели для удобства чтения.

Ключи и размеры

Вы можете задаться вопросом, почему я использовал отдельные классы для вертикального и горизонтального интервала, а не только один? Разделение осей означает, что одну можно заменить без другой. Возьмите мелодию:

Чтобы отобразить ту же самую мелодию на басовом ключе, класс stave можно заменить классом bass-stave, который сопоставляет те же атрибуты высоты данных с линиями басового нотоносца:

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

Или, с помощью CSS, который сопоставляет data-duration="5" со 120 столбцами Grid-template-columns в .bar, тому же нотному стану может быть присвоен тактовый размер 5/4:

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

Очевидно, я умалчиваю некоторые детали. Не все так просто, как смена класса, и некоторые основы и строки книги необходимо изменить.

Вот класс нотного стана, который полностью переназначает высоту звука. General MIDI помещает голоса барабанов и перкуссии в группу нот в нижних октавах клавиатуры, но эти ноты не связаны с тем, где барабаны напечатаны на нотоносце. В CSS можно определить класс барабанов-нотоносцев, который сопоставляет эти высоты тона с правильными строками:

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

Это очень читаемая нотация барабанов. Я очень доволен этим.

Аккорды и тексты песен

CSS Grid позволяет нам выравнивать и другие символы внутри сетки обозначений. Аккорды и тексты песен, динамика и т. д. могут быть выстроены в линию и охватывать синхронизированные события:

А что насчет этих балок?

Балки, хорды и некоторые более длинные паузы создаются для охвата столбцов путем сопоставления их атрибутов продолжительности данных со значениями пролета в конце столбца сетки:

.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; }
...

Просто, bru

Размеры

Наконец, вся система имеет размер em, поэтому для ее масштабирования мы просто меняем font-size:

Ограничения гибкости и сетки

Это идеальная система? Честно говоря, я просто ошеломлен тем, что это работает так хорошо, но если мы ищем предостережения… 1. CSS не может автоматически размещать новый ключ/ключ в начале каждой переносимой строки или 2. привязывать голову к новой перейдите на новую строку. И 3. угловые балки — это отдельная история; Лучи нот 1/16 и 1/32 трудно выровнять, потому что мы не можем точно знать, где находятся их основы, пока Сетка не разложит их:

Таким образом, для полного завершения работы потребуется немного привести в порядок JavaScript, но большую часть работы по макетированию здесь берет на себя CSS, а это означает, что в JavaScript нужно выполнить гораздо меньше работы по макетированию.

Дайте мне знать, что вы думаете

Если вам нравится эта система CSS или эта запись в блоге, или если вы знаете, как ее улучшить, дайте мне знать. Я зарегистрирован в Bluesky @stephen.band, Mastodon @stephband@front-end.social и в Твиттере (еще почти) @stephband. Или присоединяйтесь ко мне и сделайте это в репозитории Scribe...

&nbsp;&lt;scribe-music&gt;

Пользовательский элемент для рендеринга музыки

Я написал интерпретатор для этой новой системы CSS и обернул его в элемент <scribe-music>. Он еще далеко не готов к производству, но, поскольку он уже способен отображать отзывчивый ведущий лист и нотировать барабаны, я думаю, что это интересно и полезно.

Whazzitdo?

Элемент <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>

Или из файла, полученного по атрибуту src, например этого JSON:

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

Или из объекта JS, установленного в свойстве .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>

Как я уже сказал, он находится в разработке. Помимо некоторых непосредственных улучшений, которые я могу внести в Scribe 0.3, таких как настройка автоспеллера, исправление лучей 1/16 ноты, а также обнаружение и отображение туплетов, я хотел бы изучить некоторые долгосрочные функции:

  • Поддержка шрифтов SMuFL – изменение шрифта, используемого для обозначений. До сих пор мне не удавалось надежно отображать их расширенные наборы символов в разных браузерах.
  • Поддержка вложенных последовательностей – возможность создания многочастных мелодий.
  • Рендеринг разделенного нотоносца – размещение нескольких частей на одном нотоносце. Механика для этого уже наполовину готова — нотоносцы барабанов и фортепиано в настоящее время автоматически разделяются по высоте.
  • Рендеринг нескольких нотоносцев – размещение нескольких частей на нескольких выровненных нотоносцах.

Источник:

#CSS #HTML
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу