Хотя время 30-строчников определённо подходит к концу, уже появились примеры «Hello, world» и советы пойти в какое-нибудь другое место, неделя ещё не закончилась, и я хочу опубликовать свой вариант пианина на Javascript в ответ на творчество oshibka404 Его версия занимает целых 24 строки, то есть на медленных соединениях она будет слишком долго загружаться. Я попытался применить примерно те же идеи, но упростить код.
Хотя количество строк кода — не особо полезная мера, но я всё же думаю, что моя версия короче. Она умещается в 1134 символа как есть, а минификатор jscompress.com ужимает её до 875, а версию oshibka404 с наскоку даже в 1024 символа не запихнёшь. Кроме того, его версию JSFiddle в режиме «Tidy Up» разбивает на 46 строк, а мою — на 24.
Внимание. В этой статье я не буду объяснять теорию работы фортепиано, кто с ней не знаком, рекомендую прочесть оригинальную статью и посмотреть важную картинку. Кроме того, нужно уже понимать основы работы с HTML5 Web Audio API.
И да, ещё хочу заметить, что я сам не музыкант, да и на Javascript писать толком не умею.
Код и пояснения — под катом.
Код протестирован в Chromium 30 и Firefox 25.
Ещё можно его посмотреть на JSFiddle (или сразу поиграть).
В основе лежит идея о том, что фортепиано настраивается по ноте ля первой октавы (спасибо DrSmile за наводку). Частота звука у неё 400 герц. Частота звука любой другой ноты — 2n / 12 ∗ 440, где n — расстояние от нужной ноты до ля первой октавы в полутонах. Первая клавиша — ля субконтроктавы, отстоит от неё на 48 полутонов, как нетрудно убедиться, взглянув на изображение фортепианной клавиатуры с разделением на октавы (ссылка выше).
Изображение клавиатуры на экране представляет собой набор div'ов, стилизованных CSS. Они генерируются в виде HTML-кода функцией make(scheme, num, LWR, result), где num — номер клавиши (первая клавиша имеет номер 0), result — аккумулятор для уже сгенерированного HTML-кода, а LWR (Last-White-Right) — координата X каймы (border) последней белой клавиши — следующая белая клавиша должна примыкать непосредственно к ней, чтобы её кайма слева накладывалась на правую кайму предыдущей клавишы, а чёрная клавиша должна заезжать половиной ширины на предыдущую белую. Мы начинаем с 1, чтобы первая клавиша располагалась на отрезке от 1 до 20 по оси X, тогда её кайма будет на 0 и на 21. scheme — строка из букв W и B, которая описывает вид клавиатуры (W — белая клавиша, B — чёрная). Такой подход позволяет нам не сильно затрудняться из-за отсутствия нот си-диез и ми-диез, а также неполноты субконтроктавы и пятой октавы.
Применяются захардкоженные значения: высота белой клавиши — 300, ширина — 20 (не считая каймы), z-index = 1 (чтобы все белые были под чёрными). Высота чёрной клавишы (опять-таки не считая каймы в 1 пиксель) — 200, ширина — 10, то есть половина ширины — 5, z-index = 2. Эти те же значения, что были в оригинальной версии.
Частоту каждой клавиши мы записываем (в виде строки) ей в ID. Для краткости применяется конверсия из числа в строку при помощи '' + число, а из строки в число — при помощи строка * 1.
Функция r служит только для того, чтобы не писать String.replace.
Программисты на других языках могут не вполне понимать принцип работы операторов && и ||. && возвращает свой первый операнд, если его значение — ложь (булево false, 0, пустая строка, null, undefined), иначе — второй операнд. || возвращает первый операнд, если его значение — истина (всё, кроме лжи), иначе — второй операнд.
Ну, а далее уже дело техники. Когда пользователь кликает на любую клавишу (а это любой элемент с непустым ID), мы превращаем ID в число, и это есть частота звука, который нам надо воспроизвести.
Я надеюсь, что с этими пояснениями прочитать сам код будет нетрудно, там всего 12 строк.
Код был бы короче, если бы вместо addEventListener приделать просто каждому div'у свойство onclick=«play(частота)» (я не знаю, как правильно сделать прямые кавычки). Не надо было бы хранить частоту в ID. Но тогда это не будет работать в JSFiddle.
К оригинальному пианино в комментариях придумали несколько интересных адд-онов и расширений. Я их не стал сюда портировать, так как надо было писать как можно короче.
Изначально я попытался рисовать всё пианино на canvas'е. У меня тоже получилось 12 строк, но большая часть из них представляла собой расчётные формулы, чтобы их объяснить человеку с пятничным настроением, понадобилась бы гораздо более длинная статья с картинками. Впрочем, если кому-то интересно…
Хотя количество строк кода — не особо полезная мера, но я всё же думаю, что моя версия короче. Она умещается в 1134 символа как есть, а минификатор jscompress.com ужимает её до 875, а версию oshibka404 с наскоку даже в 1024 символа не запихнёшь. Кроме того, его версию JSFiddle в режиме «Tidy Up» разбивает на 46 строк, а мою — на 24.
Внимание. В этой статье я не буду объяснять теорию работы фортепиано, кто с ней не знаком, рекомендую прочесть оригинальную статью и посмотреть важную картинку. Кроме того, нужно уже понимать основы работы с HTML5 Web Audio API.
И да, ещё хочу заметить, что я сам не музыкант, да и на Javascript писать толком не умею.
Код и пояснения — под катом.
Где же код?
Вот тут
var aud = window.AudioContext ? new AudioContext() : new webkitAudioContext(), make = function(scheme, num, LWR, result) {
var hz = Math.pow(2, (num - 48) / 12) * 440,
code = '<div style="position: absolute; width: Wpx; height: Hpx; left: Lpx; background-color: C; border: 1px solid black; z-index: Z" id="F"></div>',
r = function(s, re, subst) { return s.replace(re, subst); };
result += (scheme[0] == 'W' && r(r(r(r(r(r(code, /W/, '20'), /H/, '300'), /L/, '' + LWR), /C/, 'white'), /F/, '' + hz), /Z/, 1) ||
r(r(r(r(r(r(code, /W/, '10'), /H/, '200'), /L/, (LWR - 5) + ''), /C/, 'black'), /F/, '' + hz), /Z/, 2));
return scheme.length == 1 && result || make(scheme.substr(1), num + 1, (scheme[0] == 'W' && LWR + 20 || LWR), result);
}; document.body.innerHTML = make('WBW!!!!!!!W'.replace(/!/g, 'WBWBWWBWBWBW'), 0, 1, "");
document.body.addEventListener('click', function(evt) {
if (evt.target.id) { var osc = aud.createOscillator(); osc.frequency.value = evt.target.id * 1; osc.type = 'square'; osc.connect(aud.destination); osc.start(0);
setTimeout(function() { osc.stop(0); osc.disconnect(aud.destination); }, 500); }
});
Код протестирован в Chromium 30 и Firefox 25.
Ещё можно его посмотреть на JSFiddle (или сразу поиграть).
Как оно работает?
В основе лежит идея о том, что фортепиано настраивается по ноте ля первой октавы (спасибо DrSmile за наводку). Частота звука у неё 400 герц. Частота звука любой другой ноты — 2n / 12 ∗ 440, где n — расстояние от нужной ноты до ля первой октавы в полутонах. Первая клавиша — ля субконтроктавы, отстоит от неё на 48 полутонов, как нетрудно убедиться, взглянув на изображение фортепианной клавиатуры с разделением на октавы (ссылка выше).
Изображение клавиатуры на экране представляет собой набор div'ов, стилизованных CSS. Они генерируются в виде HTML-кода функцией make(scheme, num, LWR, result), где num — номер клавиши (первая клавиша имеет номер 0), result — аккумулятор для уже сгенерированного HTML-кода, а LWR (Last-White-Right) — координата X каймы (border) последней белой клавиши — следующая белая клавиша должна примыкать непосредственно к ней, чтобы её кайма слева накладывалась на правую кайму предыдущей клавишы, а чёрная клавиша должна заезжать половиной ширины на предыдущую белую. Мы начинаем с 1, чтобы первая клавиша располагалась на отрезке от 1 до 20 по оси X, тогда её кайма будет на 0 и на 21. scheme — строка из букв W и B, которая описывает вид клавиатуры (W — белая клавиша, B — чёрная). Такой подход позволяет нам не сильно затрудняться из-за отсутствия нот си-диез и ми-диез, а также неполноты субконтроктавы и пятой октавы.
Применяются захардкоженные значения: высота белой клавиши — 300, ширина — 20 (не считая каймы), z-index = 1 (чтобы все белые были под чёрными). Высота чёрной клавишы (опять-таки не считая каймы в 1 пиксель) — 200, ширина — 10, то есть половина ширины — 5, z-index = 2. Эти те же значения, что были в оригинальной версии.
Частоту каждой клавиши мы записываем (в виде строки) ей в ID. Для краткости применяется конверсия из числа в строку при помощи '' + число, а из строки в число — при помощи строка * 1.
Функция r служит только для того, чтобы не писать String.replace.
Программисты на других языках могут не вполне понимать принцип работы операторов && и ||. && возвращает свой первый операнд, если его значение — ложь (булево false, 0, пустая строка, null, undefined), иначе — второй операнд. || возвращает первый операнд, если его значение — истина (всё, кроме лжи), иначе — второй операнд.
Ну, а далее уже дело техники. Когда пользователь кликает на любую клавишу (а это любой элемент с непустым ID), мы превращаем ID в число, и это есть частота звука, который нам надо воспроизвести.
Я надеюсь, что с этими пояснениями прочитать сам код будет нетрудно, там всего 12 строк.
Некоторые примечания
Код был бы короче, если бы вместо addEventListener приделать просто каждому div'у свойство onclick=«play(частота)» (я не знаю, как правильно сделать прямые кавычки). Не надо было бы хранить частоту в ID. Но тогда это не будет работать в JSFiddle.
К оригинальному пианино в комментариях придумали несколько интересных адд-онов и расширений. Я их не стал сюда портировать, так как надо было писать как можно короче.
Изначально я попытался рисовать всё пианино на canvas'е. У меня тоже получилось 12 строк, но большая часть из них представляла собой расчётные формулы, чтобы их объяснить человеку с пятничным настроением, понадобилась бы гораздо более длинная статья с картинками. Впрочем, если кому-то интересно…
Бонус
Примечание: WKW — White Key Width, TW — Total Width, TH — Total Height, BKH — Black Key Height, BKD — Black Key Distance (отступ от начала белой клавиши о следующей чёрной, например, от соль до соль-диез).
(function(){var dx, dy, img, WKW = 22, TW = WKW * 52, TH = 300, BKH = 200, BKW = 12, BKD = WKW - (BKW / 2) - 1, fl = Math.floor, cnv = document.createElement('canvas'), ctx = cnv.getContext('2d'),
key = function(x, y) { var A = fl(x / WKW), R = A % 7; return [fl(A / 7), R, y < BKH && (A && R != 2 && R != 5 && x < (A - 1) * WKW + BKD + BKW && -1 || A != 51 && R != 1 && R != 4 && x > A * WKW + BKD && 1) || 0] };
cnv.width = TW; cnv.height = TH; img = ctx.getImageData(0, 0, TW, TH);
for (var i = 0; i < TW * TH * 4; i++) {
var x = fl(i % (TW * 4) / 4), y = fl(i / TW / 4); img.data[i] = !(i % 4 != 3 && (!y || y == TH - 1 || !x || x % WKW == WKW - 1 || key(x, y)[2])) && 255 || 0;
}
ctx.putImageData(img, 0, 0); var aud = window.AudioContext ? new AudioContext() : new webkitAudioContext(), playfunc = function(evt) {
var D = key(evt.clientX - dx, evt.clientY - dy), Nd = D[1] * 2 - (D[1] >= 5 && 2 || D[1] >= 2 && 1 || 0) + D[2] - 48,
F = Math.pow(2, Nd / 12 + D[0]) * 440,
osc = aud.createOscillator(); osc.frequency.value = F; osc.type = 'square'; osc.connect(aud.destination); osc.start(0);
setTimeout(function() { osc.stop(0); osc.disconnect(aud.destination); }, 1000 / 2);
}; cnv.addEventListener('click', playfunc); document.body.appendChild(cnv); dx = cnv.offsetLeft; dy = cnv.offsetTop;})();
Примечание: WKW — White Key Width, TW — Total Width, TH — Total Height, BKH — Black Key Height, BKD — Black Key Distance (отступ от начала белой клавиши о следующей чёрной, например, от соль до соль-диез).