Поздравляю Хабр и Хаброжителей с Новым 2025 годом! Несколькими годами ранее я писал о том, как сделать ёлку из функций, в этот раз сказ пойдёт о ёлке из Uтicode символов. Ограничение — должна быть музыка, а результат должен помещаться в QR код.
Идея и ограничения
Современные браузеры поддерживают dataUrl, особый формат, хранящий все данные прямо в url. Это могут быть картинки, текст и любые другие форматы данных. Из всего этого нас интересует только текст, рассмотрим поближе:
data:[][;base64],
Поскольку dataUrl вставляется в адресную строку, это накладывает ограничения на символы, которые могут быть использованы: [a-zA-Z0-9$\-_.+!*'()] . Среди этих символов нет треугольных кавычек, необходимых для html тегов, #, необходимой для цвета, пробелов и многих других символов. Подобные символы будут заменены на escape последовательности url, начинающиеся на % (%20 — пробел).
Не смотря на то, что треугольные кавычки браузеры обрабатывают корректно, на пробеле и решётке всё равно прерывают парс (на пробеле — хром, на # — firefox), в результате пользователь попадает на страницу поиска, а не на страницу, хранящуюся в dataUrl.
Одним из решений — использовать base64, позволяющую писать любые символы, ценой увеличенного объёма в байтах.
Итоговый dataUrl занимает 22 символа и начинается на:
data:text/html;base64,
В самом большом QR-коде максимум можно записать 2953 символа, теоретически, размер QR кода не ограничен, но 2953 символа — гарантированная поддержка в большинстве сканеров.
Остаток: 2953 — 22 = 2931 символ base64 кодировки, что равняется 2931×6/8 = 2198 байтам, или ascii символам.
Для начала выровняем всё по центру в body и скроем всё, что за пределами экрана: overflow hidden, display flex, justify-content center.
В блоке div с id Y будут снежинки, а с id X будет ёлка, подарки, снеговики и текст »2025». Чтобы ёлка красиво выглядела и на широких мониторах, и на телефонах нужно ограничить размер шрифта (неявно являющимся размером ёлки) до минимума от 70% ширины экрана и 40% высоты экрана (calc (min (70vw, 40vh))) — на компе и на телефоне ёлка будет располагаться по центру экрана и не вылезать за его края.
Обязательно указываем position relative, чтобы выставлять подарки и снеговиков рядом с ёлкой.
Подарки и снеговики по своей сути — повторяющийся html код, поэтому для сокращения символов сгенерируем подарки и снеговиков через js и подставим в innerHTML блока X.
// функция для приведения числа к hex формату, нужна для цвета снеговиков и снежинок
let S = e => e.toString(16);
// так как задали id блока, можем обращаться напрямую, без document.getElementById
X.innerHTML =
// ёлка
'🎄' +
// координаты x подарков
[8, 23, 57, 72]
.map(e => `
🎁
`)
.join('')
// кординаты x и y снеговиков
+ [[82, 10], [78, 65], [95, 38]]
.map(([a, b], i) => `
☃
`)
.join('')
+ `
2025
`;
Как можно заметить, в строках повторяется много раз они и те же подстроки: div, z-index, color, вынесем их отдельно и будем подставлять через конкатенацию.
let A='
A + '20%;line-height:20%;bottom:1%;left:' + e + '%' + W + '1>🎁
')
.join('') +
[[82, 10], [78, 65], [95, 38]]
.map(([a, b], i) => A + '50%;bottom:-' + a + '%;left:' + b + '%' + J + S(8 + i * 3) + 'df' + W + '2>☃
')
.join('') +
A + '40%;top:-80%;text-align:center;width:100%;font-family:sans-serif' + J + '9df>2025
';
Вполне оптимально.
Аналогично поступим со снежинками, но будем их обновлять каждые 20 мс в блоке Y.
let V = 0;
setInterval(e => {
V++;
e = '';
for (let i = 50; i < 100; i++)
e += A + `${2 + i % 7}vh;top:${(i * 57 - 10 + V / i * 7) % 105}vh;left:${(i * 23 + V / i * 5) % 107 - 53}vw`
+ J + S(i % 7 + 5) + S(i % 5 + 9) + 'f' + W + `${i % 5}>❄
`;
Y.innerHTML = e
}, 20);
Что здесь происходит:
Каждые 20 мс вызывается функция, увеличивающая V на единицу и обновляющая все снежинки на поле. Снежинка — это ❄. Каждая снежинка в своём блоке div, с индивидуальным размером шрифта: 2 + i % 7 и псевдослучайной позицией.
Позиция по каждой из координат вычисляется на основе начальной точки, зависящей только от порядкового номера снежинки и «времени», переменной V, поделённой на порядковый номер снежинки. Из-за этого у каждой снежинки свой размер и своя скорость и траектория перемещения. Числа для умножения и нахождения остатка от деления подобраны так, чтобы глазу не был заметен паттерн. Вот что будет, если поменять значения на пару единиц:
Другие множители в формуле позиций снежинок
Текущий dataUrl — скопируйте и вставьте в адресную строку вашего браузера
N — номер ноты, j — текущий тик, D — долгота ноты (в долях, кратных 0.25)
Магическое число 3820 получается путём деления частоты дискретизации 24000 на 2 pi: 24000 / 6.2832 = 3820.
27.5 * Math.pow (1.059463, N) — это формула частоты ноты. Частота ноты увеличивается вдвое каждые 12 нот, то есть, зная частоту первой ноты можно вычислить частоты всех нот. 1.059463 — корень 12-й степени из 2.
Для преобразования .mid в более компактный вариант я написал простой скрипт, который генерирует строку из пар символов. Первый символ в каждой паре — номер ноты, второй — длительность и задержка после старта предыдущей ноты. Для компактности я добавил словарь длительностей, поэтому для его нужно будет переделывать под каждый .mid файл.
Код на питоне
import mido
import struct
midi_file = mido.MidiFile('sound.mid')
notes = []
curr_play = {}
time = 0
for i, track in enumerate(midi_file.tracks):
print(f"Дорожка {i}: {track.name if track.name else '<без имени>'}")
for msg in track:
time += msg.time
if msg.type == 'note_on':
if msg.note not in curr_play:
curr_play[msg.note] = time
elif msg.type == 'note_off':
if msg.note in curr_play:
start = curr_play[msg.note]
delta = time - start
del curr_play[msg.note]
notes.append([msg.note, start, delta])
notes.sort(key=lambda x: x[1])
print(len(notes))
binary = bytes()
prev = 0
# длительности нот
m = {1: 0, 2: 1, 6: 2, 8: 3, 12: 4, 30: 5}
for e in notes:
note, start, duration = e
start //= 96
duration //= 96
delta = start - prev
prev = start
binary += struct.pack('BB', note - 21, 35 + delta * len(m) + m[duration])
print(binary)
print(prev / 96 * 24000)
Получается такая строка
let M = `C$B/C)@*C0B/C)@*C0B/C)@*C0B/C)@*C04%B/C)@*C02%B/C)@*C00%B/C)@*C0/%B/C)@*C04%B/C)@*C02%B/C)@*C00%B/C)@*C0/%B/C)@*C0(%B/C)@*C0&%B/C)@*C0$%B/C)@*C0/%B/C)@*G04%E/G)C*G02%E/G)C*G00%E/G)C*G0/%E/G)C*L04%L/L)J)H)G*2%G/G)E)C)E*0%E/E)G)E)C*/%B/C)@*;//'=)?)@)B)C)E)G)E*C0G//'I)K)L)N)O)Q)S)Q*O0O04%N/O)L*O02%N/O)L*O00%N/O)L*O0/%N/O)L*O04%N/O)L*O02%N/O)L*O00%N/O)L*O0/%N/O)L*O04%N/O)L*O02%N/O)L*O00%N/O)L*O0/%N/O)L*O04%N/O)L*L02%L/L)L)H)G*0%G/G)E)C)E*/%E/E)G)E)C*/%B/C)@*G//'I)K)L)N)O)Q)S)Q*O0G//'I)K)L)N)O)Q)S)Q*O0O04%N/O)L*O02%N/O)L*O00%N/O)L*O0/%N/O)L*S04%Q/S)O*S02%Q/S)O*S00%Q/S)O*S0/(Q/S)O*O0N/O)L*O0N/O)L*O0N/O)L*O0N/O)L*O04(N/O)L*O0N/O)L*O0N/O)L*O0N/O)L,`
В ней много повторов :)
Музыкальный плеер
Теперь нужно воспроизвести мелодию, при загрузке страницы это сделать невозможно, только после действия пользователя, например, клика по блоку X, он достаточно большой, чтобы пользователь попал по нему пальцем :)
Код плеера на js, минифицирован.
X.onclick = (
a, // это единственный не undefined параметр, событие клика
l, t, i, j, N, I, D, W, // все undefined
P = 0,
f = new AudioContext /* js позволяет вызвать конструктор без скобок */) => {
X.onclick = null; // защита от повторных кликов
// t - созданный аудиобуфер размером 2400000 семплов и частотой дискретизации 24 кГц
// l - Float32Array, массив семплов
l = (t = f.createBuffer(1, 24e5, 24e3)).getChannelData(0);
// цикл по всем нотам, M - строка нот
for (i = 0; i < M.length; i++) {
// первый символ - нота, не теряем места и сразу переходим на следующий символ
N = M[i++].charCodeAt();
// второй - длительность и задержка
I = M[i].charCodeAt() - 35;
// длительность берётся из "словаря"
D = [1, 2, 6, 8, 12, 30][I % 6];
// а задержка считается напрямую, для взятого midi она не превышает 2
W = I / 6 | 0;
// момент начала текущей ноты. 6e3 = 6000 = 1/4 от секунды,
// при частоте дискретизации 24кГц
P += W * 6e3;
// генерация ноты
for (j = 0; j < D * 6e3; j++)
// добавляем в буфер ноту и две гармоники
l[P + j] += F(N, j, D) * .35 + F(N + 12, j, D) * .1 + F(N + 24, j, D) * .05
}
// создаём AudioBufferSourceNode, который можно проиграть,
// передаём ему буффер, в который записали семплы
(i = f.createBufferSource()).buffer = t;
// соединяем с выходом
i.connect(f.destination);
// проигрываем с начала
i.start(0);
};
Итоговый dataUrl — скопируйте и вставьте в адресную строку вашего браузера
P.S. По какой-то причине Firefox ёлка прилипает к верху, в хромеподобных браузерах (Chrome, Edge, Yandex Browser) всё отлично.
Так как ёлка, подарки и снеговики — unicode символы, то дизайн ёлки меняется от браузера к браузеру, от ОС к ОС. Покажите свою уникальную ёлку близким!