Пианино на JavaScript для Chrome

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

Зачем? Да про­сто так.

Пианино на JavaScript для Chrome

Логика работы

Про­ект состо­ит из трёх фай­лов: HTML-страницы, CSS-стилей и JS-скрипта.

HTML-страница отве­ча­ет за «мясо» про­ек­та: над­пи­си, заго­лов­ки, под­клю­че­ние зву­ков и сбор­ку все­го про­ек­та в одной точ­ке. Но стра­ни­ца сама по себе ниче­го не суме­ет: мы про­сто раз­ме­стим на ней нуж­ные бло­ки, а вся ани­ма­ция и пове­де­ние зада­ют­ся в двух дру­гих файлах.

CSS-стили реша­ют две зада­чи: оформ­ля­ют стра­ни­цу и рису­ют интер­фейс. Плав­ная ани­ма­ция нажа­тия и появ­ле­ния под­ска­зок про­пи­сы­ва­ют­ся имен­но здесь.

JS-скрипт зани­ма­ет­ся всей рабо­той пиа­ни­но: отсле­жи­ва­ет нажа­тия на кла­ви­ши, что­бы вклю­чить звук и пока­зать нуж­ный эффект на экране. 

Созда­дим и напол­ним все три фай­ла по очереди.

❗️ Этот про­ект луч­ше все­го рабо­та­ет в бра­у­зе­ре Chrome и дру­гих бра­у­зе­рах с тем же движ­ком. В Сафа­ри звук почему-то игра­ет­ся с боль­шой задерж­кой и поль­зо­вать­ся пиа­ни­но нор­маль­но не полу­ча­ет­ся. Если раз­бе­рём­ся в чём дело, выпу­стим отдель­ную ста­тью на эту тему.

HTML-страница

Вот мини­маль­ный набор того, что нам нуж­но раз­ме­стить на странице:

  1. Под­клю­чить файл со сти­ля­ми. На стар­те он может быть пустой, потом наполним.
  2. Напи­сать заго­ло­вок и под­за­го­ло­вок, что­бы было понят­но, что здесь происходит.
  3. Преду­смот­реть место для выво­да назва­ний нот.
  4. Раз­ме­тить в бло­ках <div> все клавиши.
  5. Доба­вить аудио­объ­ек­ты, что­бы обра­щать­ся к зву­ко­вым фай­лам напря­мую. Сколь­ко кла­виш, столь­ко и будет объ­ек­тов <audio>.
  6. Под­клю­чить 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-файлы мы ука­жем, но пока они будут пусты­ми, до них дой­дём на сле­ду­ю­щих этапах.

index.html

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

Пианино на JavaScript для Chrome Резуль­тат не похож на пиа­ни­но, пото­му что у нас нет сти­лей и скрип­та. Но это пока 

Настраиваем 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;
}

Сле­ду­ю­щий блок — настро­им внеш­ний вид всех кла­виш. У нас будут четы­ре раз­ных стиля:

  1. Общие пра­ви­ла рас­по­ло­же­ния для все­го бло­ка кла­виш — раз­ме­ры бло­ка, поло­же­ние на стра­ни­це, пра­ви­ла для вло­жен­ных элементов.
  2. Общие пра­ви­ла оформ­ле­ния для каж­дой кла­ви­ши — ради­ус скруг­ле­ния, гра­ни­цы, поря­док слоёв.
  3. Два пра­ви­ла оформ­ле­ния отдель­но белых и чёр­ных клавиш.

/* общие настройки для всего блока клавиш */
.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;
}
Пианино на JavaScript для Chrome Выгля­дит кра­си­во, но пока ниче­го не работает 
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;
}

/* настройки смещения текста подсказок, чтобы они оказались на каждой клавише*/
.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");

Теперь сде­ла­ем основ­ную функ­цию, кото­рая вклю­чит опре­де­лё­ный звук в нуж­ный момент. Логи­ка будет такая:

  1. Полу­ча­ем код нажа­той клавиши.
  2. По это­му коду нахо­дим кла­ви­шу пиа­ни­но и добав­ля­ем ей ани­ма­цию нажа­тия. Заод­но берём из опи­са­ния кла­ви­ши текст ноты и пока­зы­ва­ем его на экране.
  3. По это­му же коду нажа­той кла­ви­ши нахо­дим нуж­ный аудио­файл и про­иг­ры­ва­ем его.

// проигрываем звук при нажатии на клавишу
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);

Теперь мож­но собрать все фай­лы вме­сте и послу­шать, как зву­чит наше пиа­ни­но, на стра­ни­це про­ек­та.

Таким же обра­зом мож­но сде­лать и дру­гие вир­ту­аль­ные инстру­мен­ты — бара­ба­ны, элек­трон­ный сем­плер или что угод­но ещё. Зай­мём­ся этим в сле­ду­ю­щий раз.

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();
}

// функция, которая убирает анимацию нажатия на клавишу
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);

Код:

Кэро­лайн Габриэль

Текст:

Миха­ил Полянин

Редак­тор:

Мак­сим Ильяхов

Худож­ник:

Даня Бер­ков­ский

Кор­рек­тор:

Ири­на Михеева

Вёрст­ка:

Мария Дро­но­ва

Соц­се­ти:

Олег Веш­кур­цев