Мы нашли готовый чужой код и немного его доработали, чтобы он стал проще и понятнее. В результате у нас получилось некое подобие программного пианино, сейчас покажем внутренности.
Зачем? Да просто так.
Логика работы
Проект состоит из трёх файлов: HTML-страницы, CSS-стилей и JS-скрипта.
HTML-страница отвечает за «мясо» проекта: надписи, заголовки, подключение звуков и сборку всего проекта в одной точке. Но страница сама по себе ничего не сумеет: мы просто разместим на ней нужные блоки, а вся анимация и поведение задаются в двух других файлах.
CSS-стили решают две задачи: оформляют страницу и рисуют интерфейс. Плавная анимация нажатия и появления подсказок прописываются именно здесь.
JS-скрипт занимается всей работой пианино: отслеживает нажатия на клавиши, чтобы включить звук и показать нужный эффект на экране.
Создадим и наполним все три файла по очереди.
❗️ Этот проект лучше всего работает в браузере Chrome и других браузерах с тем же движком. В Сафари звук почему-то играется с большой задержкой и пользоваться пианино нормально не получается. Если разберёмся в чём дело, выпустим отдельную статью на эту тему.
HTML-страница
Вот минимальный набор того, что нам нужно разместить на странице:
- Подключить файл со стилями. На старте он может быть пустой, потом наполним.
- Написать заголовок и подзаголовок, чтобы было понятно, что здесь происходит.
- Предусмотреть место для вывода названий нот.
- Разметить в блоках <div> все клавиши.
- Добавить аудиообъекты, чтобы обращаться к звуковым файлам напрямую. Сколько клавиш, столько и будет объектов <audio>.
- Подключить JS-скрипт. Он тоже может быть пока пустой, главное сейчас, чтобы на странице было всё, что нам понадобится дальше.
Первые три пункта простые, а вот в пункте 4 есть нюанс. Нам нужно разместить клавиши так, чтобы у каждой клавиши были прописаны:
- код клавиши на клавиатуре, которая за неё отвечает;
- класс — это чёрная или белая клавиша на пианино;
- нота, которая прозвучит при нажатии;
- буквенная подсказка, чтобы было понятно, какая клавиша на клавиатуре за что отвечает на пианино.
Сделаем это на блоках <div> и посмотрим на структуру. Этот код нельзя использовать, потому что мы его прокомментировали с помощью стрелочек. Но можно прочитать, что тут имеется в виду:
<div
data-key=»65″ ← код клавиши на клавиатуре компьютера
class=»key» ← это белая клавиша, у чёрных клавиш класс key sharp
data-note=»C» ← нота, которая появится на экране при нажатии
>
<span ← обернём подсказку в свой тег
class=»hints»> ← класс оформления подсказки
A ← какую клавишу нужно нажать на клавиатуре
</span>
</div>
Если собрать всё вместе, получится так:
<div data-key=»65″ class=»key» data-note=»C»>
<span class=»hints»>A</span>
</div>
Аудио на странице мы уже подключали, когда делали проект про «вжух» на любой странице. Там при любом движении мышкой запускался звук острого меча — попробуйте сами открыть страницу и подвигать мышью.
Но в тот раз мы подключали аудио варварским способом — через добавление в коде скрипта, потому что не было доступа к странице. Сейчас у нас есть полный доступ, поэтому аудио можно добавить так:
<audio data-key="65" src="040.wav"></audio>
Обратите внимание на свойство data-key — оно совпадает со свойством у клавиши, которую мы разместили в блоке <div>. Это нужно для того, чтобы по коду найти и нужную клавишу пианино, и звук, за который она отвечает.
Таким же образом нужно будет добавить все остальные звуки. Чтобы всё работало быстрее, мы положим аудиофайлы в ту же папку, что и HTML-документ. Эти файлы мы взяли у того же разработчика, у которого утащили и остальной код — Кэролайн Габриель .
Теперь соберём готовую страницу. CSS- и JS-файлы мы укажем, но пока они будут пустыми, до них дойдём на следующих этапах.
<!DOCTYPE html>
<html lang="ru" >
<head>
<meta charset="UTF-8">
<title>Пианино на JavaScript</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<body>
<section>
<!-- заголовок и пояснение -->
<h1>Пианино на JavaScript</h1>
<h2>Используйте клавиатуру, наведите курсор для подсказок.</h2>
<!-- основная секция -->
<section>
<!-- сюда будем выводить название ноты, клавишу которой мы нажали-->
<div class="nowplaying"></div>
<!-- блок с клавишами -->
<div class="keys">
<!-- структура такая: код на клавиатуре → название класса → название ноты → текст подсказки -->
<div data-key="65" class="key" data-note="C">
<span class="hints">A</span>
</div>
<div data-key="87" class="key sharp" data-note="C#">
<span class="hints">W</span>
</div>
<div data-key="83" class="key" data-note="D">
<span class="hints">S</span>
</div>
<div data-key="69" class="key sharp" data-note="D#">
<span class="hints">E</span>
</div>
<div data-key="68" class="key" data-note="E">
<span class="hints">D</span>
</div>
<div data-key="70" class="key" data-note="F">
<span class="hints">F</span>
</div>
<div data-key="84" class="key sharp" data-note="F#">
<span class="hints">T</span>
</div>
<div data-key="71" class="key" data-note="G">
<span class="hints">G</span>
</div>
<div data-key="89" class="key sharp" data-note="G#">
<span class="hints">Y</span>
</div>
<div data-key="72" class="key" data-note="A">
<span class="hints">H</span>
</div>
<div data-key="85" class="key sharp" data-note="A#">
<span class="hints">U</span>
</div>
<div data-key="74" class="key" data-note="B">
<span class="hints">J</span>
</div>
<div data-key="75" class="key" data-note="C">
<span class="hints">K</span>
</div>
<div data-key="79" class="key sharp" data-note="C#">
<span class="hints">O</span>
</div>
<div data-key="76" class="key" data-note="D">
<span class="hints">L</span>
</div>
<div data-key="80" class="key sharp" data-note="D#">
<span class="hints">P</span>
</div>
<div data-key="186" class="key" data-note="E">
<span class="hints">;</span>
</div>
</div>
<!-- аудиофайлы, каждый из них отвечает за свою ноту -->
<audio data-key="65" src="040.wav"></audio>
<audio data-key="87" src="041.wav"></audio>
<audio data-key="83" src="042.wav"></audio>
<audio data-key="69" src="043.wav"></audio>
<audio data-key="68" src="044.wav"></audio>
<audio data-key="70" src="045.wav"></audio>
<audio data-key="84" src="046.wav"></audio>
<audio data-key="71" src="047.wav"></audio>
<audio data-key="89" src="048.wav"></audio>
<audio data-key="72" src="049.wav"></audio>
<audio data-key="85" src="050.wav"></audio>
<audio data-key="74" src="051.wav"></audio>
<audio data-key="75" src="052.wav"></audio>
<audio data-key="79" src="053.wav"></audio>
<audio data-key="76" src="054.wav"></audio>
<audio data-key="80" src="055.wav"></audio>
<audio data-key="186" src="056.wav"></audio>
</section>
</section>
</body>
<!-- основной скрипт -->
<script src="script.js"></script>
</body>
</html>
Настраиваем CSS-стили
Так как стили хранятся в отдельном файле style.css, сразу создадим его и пропишем общие настройки для всей страницы, а также стили заголовка и подзаголовка. Ещё добавим отдельный стиль для внешнего вида проигрываемой ноты, которая появляется над клавишами пианино.
/* общие настройки вида страницы */
html {
background: #000;
font-family: 'Noto Serif', serif;
-webkit-font-smoothing: antialiased;
text-align: center;
}
/* заголовок */
h1 {
color: #fff;
font-size: 50px;
font-weight: 400;
letter-spacing: 0.18em;
text-transform: uppercase;
margin: 0;
}
/* подзаголовок */
h2 {
color: #fff;
font-size: 24px;
font-style: italic;
font-weight: 400;
margin: 0 0 30px;
}
/* настройки внешнего вида ноты, которая появляется над клавишами */
.nowplaying {
font-size: 120px;
line-height: 1;
color: #eee;
text-shadow: 0 0 5rem #028ae9;
transition: all .07s ease;
min-height: 120px;
}
Следующий блок — настроим внешний вид всех клавиш. У нас будут четыре разных стиля:
- Общие правила расположения для всего блока клавиш — размеры блока, положение на странице, правила для вложенных элементов.
- Общие правила оформления для каждой клавиши — радиус скругления, границы, порядок слоёв.
- Два правила оформления отдельно белых и чёрных клавиш.
/* общие настройки для всего блока клавиш */
.keys {
display: block;
width: 100%;
height: 350px;
max-width: 880px;
position: relative;
margin: 40px auto 0;
cursor: none;
}
/* общие настройки внешнего вида клавиш */
.key {
position: relative;
border: 4px solid black;
border-radius: .5rem;
transition: all .07s ease;
display: block;
box-sizing: border-box;
z-index: 2;
}
/* внешний вид белых клавиш */
.key:not(.sharp) {
float: left;
width: 10%;
height: 100%;
background: rgba(255, 255, 255, .8);
}
/* внешний вид и положение чёрных клавиш */
.key.sharp {
position: absolute;
width: 6%;
height: 60%;
background: #000;
color: #eee;
top: 0;
z-index: 3;
}
Теперь подсказки — нам нужно разместить каждую подсказку точно над своей клавишей пианино, поэтому мы задаём процент смещения для каждой из них от начала блока. Посмотрите на уже знакомое свойство data-key — по нему стили определяют, какое смещение к какой клавише относится:
/* настройки смещения текста подсказок, чтобы они оказались на каждой клавише*/
.key[data-key="87"] {
left: 7%;
}
.key[data-key="69"] {
left: 17%;
}
.key[data-key="84"] {
left: 37%;
}
.key[data-key="89"] {
left: 47%;
}
.key[data-key="85"] {
left: 57%;
}
.key[data-key="79"] {
left: 77%;
}
.key[data-key="80"] {
left: 87%;
}
Последнее, что нам осталось тут сделать, — добавить анимацию. За это у нас будут отвечать три блока: первый подсветит нажатую клавишу, второй настроит красивое и плавное появление подсказок, а третий включит подсказки в нужный момент:
/* подсвечиваем нажатую клавишу */
.playing {
transform: scale(.95);
border-color: #028ae9;
box-shadow: 0 0 1rem #028ae9;
}
/* настройка внешнего вида подсказок на клавишах */
.hints {
display: block;
width: 100%;
opacity: 0;
position: absolute;
bottom: 7px;
transition: opacity .3s ease-out;
font-size: 20px;
}
/* включаем подсказки на клавишах при наведении курсора */
.keys:hover .hints {
opacity: 1;
}
/* общие настройки вида страницы */
html {
background: #000;
font-family: 'Noto Serif', serif;
-webkit-font-smoothing: antialiased;
text-align: center;
}
/* заголовок */
h1 {
color: #fff;
font-size: 50px;
font-weight: 400;
letter-spacing: 0.18em;
text-transform: uppercase;
margin: 0;
}
/* подзаголовок */
h2 {
color: #fff;
font-size: 24px;
font-style: italic;
font-weight: 400;
margin: 0 0 30px;
}
/* настройки внешнего вида ноты, которая появляется над клавишами */
.nowplaying {
font-size: 120px;
line-height: 1;
color: #eee;
text-shadow: 0 0 5rem #028ae9;
transition: all .07s ease;
min-height: 120px;
}
/* общие настройки для всего блока клавиш */
.keys {
display: block;
width: 100%;
height: 350px;
max-width: 880px;
position: relative;
margin: 40px auto 0;
cursor: none;
}
/* общие настройки внешнего вида клавиш */
.key {
position: relative;
border: 4px solid black;
border-radius: .5rem;
transition: all .07s ease;
display: block;
box-sizing: border-box;
z-index: 2;
}
/* внешний вид белых клавиш */
.key:not(.sharp) {
float: left;
width: 10%;
height: 100%;
background: rgba(255, 255, 255, .8);
}
/* внешний вид и положение чёрных клавиш */
.key.sharp {
position: absolute;
width: 6%;
height: 60%;
background: #000;
color: #eee;
top: 0;
z-index: 3;
}
/* настройки смещения текста подсказок, чтобы они оказались на каждой клавише*/
.key[data-key="87"] {
left: 7%;
}
.key[data-key="69"] {
left: 17%;
}
.key[data-key="84"] {
left: 37%;
}
.key[data-key="89"] {
left: 47%;
}
.key[data-key="85"] {
left: 57%;
}
.key[data-key="79"] {
left: 77%;
}
.key[data-key="80"] {
left: 87%;
}
/* подсвечиваем нажатую клавишу */
.playing {
transform: scale(.95);
border-color: #028ae9;
box-shadow: 0 0 1rem #028ae9;
}
/* настройка внешнего вида подсказок на клавишах */
.hints {
display: block;
width: 100%;
opacity: 0;
position: absolute;
bottom: 7px;
transition: opacity .3s ease-out;
font-size: 20px;
}
/* включаем подсказки на клавишах при наведении курсора */
.keys:hover .hints {
opacity: 1;
}
Пишем скрипт
Создадим новый файл script.js и пропишем три объекта, с которыми будем работать:
// получаем все объекты на странице с классом .key — это наши клавиши
const keys = document.querySelectorAll(".key"),
// получаем область на странице, куда будем выводить названия нот
note = document.querySelector(".nowplaying"),
// тут хранятся все наши подсказки
hints = document.querySelectorAll(".hints");
Теперь сделаем основную функцию, которая включит определёный звук в нужный момент. Логика будет такая:
- Получаем код нажатой клавиши.
- По этому коду находим клавишу пианино и добавляем ей анимацию нажатия. Заодно берём из описания клавиши текст ноты и показываем его на экране.
- По этому же коду нажатой клавиши находим нужный аудиофайл и проигрываем его.
// проигрываем звук при нажатии на клавишу
function playNote(e) {
// получаем аудиообъект по коду нажатой клавиши
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`),
// получаем нажатую клавишу на пианино по коду нажатой клавиши на клавиатуре
key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
// если мы нажали клавишу, которой не было в списке, то выходим из функции и никакой звук не играем
if (!key) return;
// получаем название ноты
const keyNote = key.getAttribute("data-note");
// добавляем класс, который отвечает за анимацию нажатия
key.classList.add("playing");
// выводим на экран название ноты
note.innerHTML = keyNote;
// будем проигрывать каждое аудио с самого начала
audio.currentTime = 0;
// включаем звук нажатой клавиши
audio.play();
}
Для вызова функции добавим в скрипт обработчик нажатий:
// отслеживаем нажатие каждой клавиши и сразу включаем звук
window.addEventListener("keydown", playNote);
У нас есть анимация нажатия — у клавиши появляется синий контур и небольшая тень. Но нам нужно убрать этот эффект сразу, как он закончит отрисовываться, чтобы клавиша как будто вернулась в исходное состояние. Для этого добавим в скрипт такой код:
// функция, которая убирает анимацию нажатия на клавишу
function removeTransition(e) {
// если у клавиши уже нет свойства transform
if (e.propertyName !== "transform") return;
// убираем класс playing из описания клавиши
this.classList.remove("playing");
}
// перебираем все клавиши, где запустилась анимация, и убираем обводку с тех клавиш, где она уже закончила отрисовываться
keys.forEach(key => key.addEventListener("transitionend", removeTransition));
У нас осталась только анимация подсказок над клавишами. Мы не запустим её из самого скрипта, зато можем добавить каждой подсказке своё время задержки. Это даст эффект плавного появления. Чем правее клавиша пианино, тем больше её порядковый номер, а значит, и время появления на экране:
// функция отображения подсказок
function hintsOn(e, index) {
// показываем на экране все подсказки с плавной задержкой слева направо
// время задержки зависит от позиции клавиши
e.setAttribute("style", "transition-delay:" + index * 50 + "ms");
}
// включаем отображение подсказок
hints.forEach(hintsOn);
Теперь можно собрать все файлы вместе и послушать, как звучит наше пианино, на странице проекта.
Таким же образом можно сделать и другие виртуальные инструменты — барабаны, электронный семплер или что угодно ещё. Займёмся этим в следующий раз.
// получаем все объекты на странице с классом .key — это наши клавиши
const keys = document.querySelectorAll(".key"),
// получаем область на странице, куда будем выводить названия нот
note = document.querySelector(".nowplaying"),
// тут хранятся все наши подсказки
hints = document.querySelectorAll(".hints");
// проигрываем звук при нажатии на клавишу
function playNote(e) {
// получаем аудиообъект по коду нажатой клавиши
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`),
// получаем нажатую клавишу на пианино по коду нажатой клавиши на клавиатуре
key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
// если мы нажали клавишу, которой не было в списке, то выходим из функции и никакой звук не играем
if (!key) return;
// получаем название ноты
const keyNote = key.getAttribute("data-note");
// добавляем класс, который отвечает за анимацию нажатия
key.classList.add("playing");
// выводим на экран название ноты
note.innerHTML = keyNote;
// будем проигрывать каждое аудио с самого начала
audio.currentTime = 0;
// включаем звук нажатой клавиши
audio.play();
}
// функция, которая убирает анимацию нажатия на клавишу
function removeTransition(e) {
// если у клавиши уже нет свойства transform
if (e.propertyName !== "transform") return;
// убираем класс playing из описания клавиши
this.classList.remove("playing");
}
// функция отображения подсказок
function hintsOn(e, index) {
// добавляем каждой подсказке новое свойство, которое отвечает за задержку появления
// время задержки зависит от позиции клавиши
e.setAttribute("style", "transition-delay:" + index * 50 + "ms");
}
// включаем отображение подсказок
hints.forEach(hintsOn);
// перебираем все клавиши, где запустилась анимация, и убираем обводку с тех клавиш, где она уже закончила отрисовываться
keys.forEach(key => key.addEventListener("transitionend", removeTransition));
// отслеживаем нажатие каждой клавиши и сразу включаем звук
window.addEventListener("keydown", playNote);