Как поиграть в QR код
Привет!
QR коды - очень крутая штука. Но что если я скажу вам, что в них можно еще и поиграть. Интересно? Тогда поехали!
Итак, сегодня я расскажу о том как я написал игру flappy bird и поместил ее в QR код. Задача не такая простая как кажется на первый взгляд. В качестве челленджа мы хотим, чтобы вся игра содержалась внутри QR кода и для того чтобы поиграть нам был нужен только QR код, то есть нам не подходит вариант с ссылкой на игру внутри QR кода. Это неинтересно!
Чаще всего, когда вы сканируете QR код данные из декодера попадают в поисковую строку браузера. Конечно, есть и другие сценарии использования, однако я пока предлагаю остановиться на этом. Значит наша задача заключается в том, чтобы передать исходный код нашей игры с через строку браузера. Но можем ли мы так сделать? Конечно, можем и помогут нам в этом Data URLs. Мы можем проверить работоспособность данного метода просто взяв любой валидный HTML код, закодировав его в base64 и добавив вначале префикс
. Далее нам нужно просто получившуюся строку закодировать в QR код. Звучит довольно просто, но здесь есть одно не очень приятное обстоятельство: мы не можем закодировать в QR код бесконечное количество данных.data:text/html;base64,
Ограничения
По спецификации QR коды делятся на версии. Номера версий варьируются от 1 до 40. Каждая версия имеет особенности в конфигурации и количестве точек(модулей) составляющих QR-код. Версия 1 содержит 21×21 модулей, версия 40 — 177×177. От версии к версии размер кода увеличивается на 4 модуля на сторону.
Каждой версии соответствует определенная емкость с учетом уровня коррекции ошибок. Чем больше информации необходимо закодировать и чем больший уровень избыточности используется, тем большая версия кода нам потребуется.
QR код поддерживает несколько режимов кодирования:
Mode | Characters | Compression |
---|---|---|
Числовой | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 | 3 символа представлены 10 битами |
Буквенно-цифровой | 0–9, A–Z (только верхний регистр), пробел, $, %, *, +, -, ., /, : | 2 символа представлены 11 битами |
Байт | Символы из набора ISO/IEC 8859-1 | Каждый символ представлен 8 битами |
Кандзи | Символы из системы Shift JIS, основанной на JIS X 0208 | 2 кандзи символа представлены 13 битами |
Также, QR-коды имеют специальный механизм увеличения надежности хранения зашифрованной информации. Для кодов созданных с самым высоким уровнем надежности могут быть испорчены или затерты до 30% поверхности, но они сохранят информацию и будут корректно прочитаны. Для исправления ошибок используется алгоритм Рида-Соломона.
В таблице ниже показано максимальное количество сохраняемых символов в каждом режиме кодирования и для каждого уровня исправления ошибок:
Mode | L (~7%) | M (~15%) | Q (~25%) | H (~30%) |
---|---|---|---|---|
Числовой | 7089 | 5596 | 3993 | 3057 |
Буквенно-цифровой | 4296 | 3391 | 2420 | 1852 |
Байт | 2953 | 2331 | 1663 | 1273 |
Кандзи | 1817 | 1435 | 1024 | 784 |
Алфавит base64 содержит 64 символа и разделен на группы:
- Заглавные буквы (индексы 0-25): A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z
- Строчные буквы (индексы 26-51): a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z
- Цифры (индексы 52-61): 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
- Специальные символы (индексы 62-63): +, /
In addition to these characters, the equal sign (=
) is used for padding.
Помимо этих символов, используется знак равенства (=) в качестве заполнителя.
Самый оптимальный режим кодирования для моей задачи - это побайтовый. Что касается уровня исправления ошибок я думаю M уровня будет достаточно, поэтому мы можем рассчитывать на то, что сможем закодировать 2331 символов, это чуть больше 2 килобайт. Однако, следует помнить, что закодированная в base64 строка длиннее исходной примерно на 30%.
Инструменты
Для удобства разработки необходимо оптимизировать процесс генерации QR кода. Также, необходимо произвести некоторую предобработку исходного HTML кода, а именно минификацию, так как у нас есть строгое ограничение по размеру.
Для этих целей был написан специальный скрипт на NodeJS:
import { minify } from 'html-minifier';
import { readFileSync, writeFileSync } from 'fs';
import { resolve, join } from 'path';
import { toFile } from 'qrcode';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
const kb = (bytes) => (bytes / 1024).toFixed(2);
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const input = readFileSync(resolve(__dirname, '../index.html'), { encoding: 'utf-8' });
console.log('Original size is', kb(input.length), 'KB', `(${input.length} bytes)`);
const minified = minify(input, {
collapseWhitespace: true,
minifyCSS: true,
minifyJS: {
mangle: {
// переменные на верхнем уровне
// по-умолчанию не минифицируются
toplevel: true
}
}
});
console.log('Minified size is', kb(minified.length), 'KB', `(${minified.length} bytes)`);
const outDir = resolve(__dirname, '../output');
execSync(`mkdir -p ${outDir}`);
writeFileSync(join(outDir, 'index.min.html'), minified);
const b64 = Buffer.from(minified).toString('base64');
const output = `data:text/html;base64,${b64}`;
console.log('b64 size is', kb(output.length), 'KB', `(${output.length} bytes)`);
writeFileSync(join(outDir, 'b64.txt'), output);
toFile(
join(outDir, 'image.png'),
[{ data: output, mode: 'byte' }], // режим кодирования
{ errorCorrectionLevel: 'm' } // уровнь исправления ошибок
);
console.log('Done!');
Таким образом, после запуска скрипта мы имеем на выходе готовый QR код, а также детализацию по размеру кода, что поможет нам в дальнейшей оптимизации.
Разработка
Для начала создадим минимальный валидный HTML документ:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Flappy bird</title>
</head>
<body>
</body>
</html>
Я хотел бы чтобы в игре были как можно более адекватные текстуры, однако мы договорились не загружать ничего дополнительно, поэтому придется программировать рисование текстур. Можно было бы развлекаться с div
, box-shadow
, различными градиентами или SVG, но все эти подходы достаточно многословны и особо нет эффективных способов минифицировать такой код. Поэтому я решил выбрать canvas
:
...
<body>
<canvas id="c" width="360" height="640"></canvas>
</body>
...
Я создал DOM элемент и установил атрибут id
.
Теперь в коде я могу просто обратиться к нему как window.c
или просто по имени c
и нет никакой необходимости в использовании методов поиска DOM элементов в документе по типу querySelector
или getElementById
, имена которых на самом деле достаточно длинные и не поддаются сокращению. Кстати, хотел бы обратить внимание на этот момент. Мы будем использовать canvas
, то есть для рисования мы будем использовать различные методы CanvasRenderingContext2D
, и есть смысл присваивать эти методы в переменные, потому что минифицировать свойства объекта мы не можем, однако для присваивания нам необходимо вызвать bind
, чтобы передать this
контекст, коим в данном случае является контекст рендеринга canvas
. Вот о чем я говорю:
let ctx = c.getContext('2d');
ctx.fillRect(...)
ctx.fillRect(...)
ctx.fillRect(...)
После минификации даст что-то типа такого (проблельные символы оставлены для понятности):
let a = c.getContext('2d');
a.fillRect(...)
a.fillRect(...)
a.fillRect(...)
В то время как такой кусок кода:
let ctx = c.getContext('2d');
let fillRect = ctx.fillRect.bind(ctx);
fillRect(...)
fillRect(...)
fillRect(...)
Превратится во что-то типа такого:
let a = c.getContext('2d'), b = a.fillRect.bind(a);
b(...)
b(...)
b(...)
То есть профит от такого присвоения есть, но не всегда. Для каждого отдельного случая нужно считать длину строк. Например, для метода fillRect
такое присвоение имеет смысл только если этот метод вызывается в коде 2 или более раз.
Конечно, есть и другие способы присвоения функции, например:
let fillRect = (...args) => ctx.fillRect(...args);
Но такой вариант будет длиннее после минификации по сравнению с кодом, где используется
. Поэтому я решил использовать код с bind
, так как не смог найти вариант, который был бы короче.bind
Кстати, повсеместно в коде используется
, потому что он на 2 символа короче let
🙂.const
Текстуры
Я уже говорил о том, что хотел бы использовать как можно более похожие на оригинал текстуры для своей игры. Например, примерно такую птицу я бы хотел видеть в игре:
При этом у нас нет возможности загрузить картинки через интернет, поэтому я посчитал, что самым коротким способом будет хранить изображения в переменных типа string прямо в исходном коде. Но как уместить картинки в 1.6 килобайта с учетом того, что есть еще и код игры?
Можно заметить, что для того, чтобы нарисовать текстуру выше нам нужно всего 5 цветов. В то время как популярные форматы изображений такие как JPEG или PNG позволяют кодировать миллионы цветов, что достигается достаточно высокой разрядностью цвета. Например, цветные изображения JPEG хранят 24 бита на пиксель, а PNG до 48 бит. Поэтому вариант с тем чтобы просто закодировать картинку в base64 сразу отпал.
Такая глубина цвета нам не нужна, а значит решить эту проблему можно с помощью разработки собственной палитры. Давайте представим, что каждому цвету мы назначили букву латинского алфавита, тогда изображение выше примет вид:
uuuuuuaaaaaauuuuu
uuuuaacccabbauuuu
uuuaccccabbbbauuu
uuacccccabbbabauu
uaccccccabbbabauu
uaaaaacccabbbbauu
adddddacccaaaaaau
adddddaccaeeeeeea
uaaaaaccaeaaaaaau
uuaccccccaeeeeeau
uuuaacccccaaaaauu
uuuuuaaaaauuuuuuu
legend:
u - transparent (не знаю почему u, видимо потому что undefined)
a - #000
b - #fff
c - #f71
d - #eec
e - #f20
Размер такой картинки - 204 байта. Однако, только посмотрев на эту картинку виден огромный потенциал для сжатия! Первое, что приходит на ум - это RLE. Если коротко, то суть алгоритма заключается в замене повторяющихся символов на символ + кол-во повторов: `aaaaa` -> `a5`. В случае с последовательностью выше сжатие работает достаточно эффективно - 32.35 %. Минус такого подхода в том, что при неповторяющихся последовательностях алгоритм работает в другую сторону: `abcde` -> `a1b1c1d1e1`, хотя это и поддается оптимизации: `abcdeeeee` -> `-4abcd5e` или `abcdeee` -> `abcd3e`
Давайте рассмотрим другой способ. Допустим цветов у нас будет не больше 16, тогда можно особо не напрягаясь достигнуть сжатия 50% для ЛЮБОГО набора данных. Каждому символу необходимо присвоить код. И в результирующую строку записывать не сам символ, а результат выражения:
char((code(color1) << 4) | code(color2))
// char - получения символа по его коду
// code - получение кода по символу
Тогда в 8 битах мы будем хранить информацию о 2 пикселях. Но есть один нюанс. Коды символам нужно присваивать с 2 (0b10
). Почему? В таблице ASCII символов до кода 32 идут служебные символы. Я не нашел способа их напечатать на Mac, а также не уверен относительно того можно ли использовать их в скрипте. Скорее всего, можно, но потребуется экранирование. В общем, для решения этой проблемы достаточно добавить смещение 2 и тогда в результате функции code
будет всегда получаться число больше 32, а значит и печатаемый символ.
В результате с помощью первого алгоритма (RLE с оптимизацией единичных символов) я получил:
u6a6u9a2c3ab2au7ac4ab4au5ac5ab3abau3ac6ab3abau3a5c3ab4au2ad5ac3a6uad5ac2ae6aua5c2aea6u3ac6ae5au4a2c5a5u7a5u7
А в результате второго:
ÌÌÌ333ÌÌÌÌÃ5U4CÌÌÌÃUU4DCÌÌÃUUSDCCÌÃUUU4D4<Ì335U4DCÌ6ff5U333ÃffcU7wwsÃ33U7333ÌÃUUU7ww<ÌÃ5UU33<ÌÌÌ33<ÌÌÌ
И разница составила всего 6 байт. Конечно, результат второго алгоритма можно попробовать еще прогнать через RLE, но надо помнить о том, что раскодировать это все тоже придется кодом скрипта, который тоже занимает какое то место и я решил, что RLE меня вполне устраивает.
Кстати, можно было бы попробовать закодировать исходную строку алгоритмом Хаффмана, и даже заранее вычислить кодовое дерево, но в таком случае довольно трудно контроллировать непоявление служебных непечатаемых символов, а код реализации декодера достаточно громоздкий по сравнению с кодом декодера RLE или битовых последовательностей.
Таким образом, в игре появились текстуры:
let birdTexture = `a6u8a2c3ab2au6ac4ab4au4ac5ab3abau2ac6ab3abau2ac7ab4au2ac8a6uac7ad6auac5ada6u2ac6ad5au3a2c5a5u6a5`;
let wing1Texture = `a4u2ae4auae5a2e5auae3au3a3`;
let wing2Texture = `a5uae5a2e5aua5u`;
let wing3Texture = `a5uae5a2e4auae3au3a4`;
let pipeTexture = 'g6h4g4h9h3g4h4g9g4f9f3';
let widePipeTexture = 'g3' + pipeTexture + 'f3';
После минификации этот код будет весить примерно 220 байт, что вполне приемлемо.
Но пока что это всего лишь обычные строки. Для их превращения в изображения тоже нужно проделать определенные действия:
let draw = (t, x, y, w, i = 0, pw = 2, ph = 2) => {
while (t.length) {
let c = t[0], times = +t[1];
// Повтороений не должно быть больше 9
// То есть вместо записи a10 можно записать a5a5
fillStyle(c);
t = t.slice(times !== times ? 1 : 2);
if (c === 'u') i += (times || 1);
else for (let until = (times || 1) + i; i < until; i++) fillRect(x + (i % w) * pw, y + ((i / w) | 0) * pw, pw, ph);
}
}
Итак, что же здесь происходит? Первым аргументом мы передаем текстуру - одну из тех строк, которые мы рассмотрели выше. Далее идут координаты x
и y
- это смещение рисования относительно левого верхнего угла canvas. Так как текстура у нас представлена строкой необходимо указать w
(ширину), это позволит вовремя переносить рисование на “следующую строку”. Аргумент i
тоже заведен с целью оптимизации - если текстура начинается с пустых пикселей ('u'
), то их можно не указывать, а счетчик i
сразу перенести на кол-во таких пикселей. Благодаря этому мы можем избавляться от 'u'
как мы избавляемся от незначащих нулей в числе. pw
, py
- ширина и высота пикселя соответсвтенно. Так как бОльшая часть трубы выглядит таким образом:
Мы можем вызвать draw
всего один раз и указать ph
, равный высоте трубы, вместо N вызовов draw
, где N высота трубы в пикселях / ph
. Сделано для быстродействия.
Код
Вся игра у нас (как и любая другая, насколько мне известно) будет крутиться в бесконечном цикле:
let tick = 0;
let setup = () => {
vSpeed = flyUpSpeed; // птица взлетает в самом начале
score = playing = 0;
y = 308; // начальное положение - центр экрана
pipes = [[100, gate()], [788, gate()]];
// инициализируем первые 2 трубы,
// про трубы чуть дальше раскажу подробнее
}
let render = () => {
...
tick++;
requestAnimationFrame(render);
}
setup();
render();
Первым делом, конечно, надо нарисовать фон. Я просто заливаю его цветом #0ac
(цвета используем трехбуквенные). Я думал насчет генерации фона, но не придумал как уместить этот код в наш размер, поэтому просто залил. Далее идет рисование труб:
pipes.map(pipe => {
pipe[0] -= hSpeed; // двигаем трубы каждый рендер
let [px, py] = pipe;
draw('a2' + pipeTexture + 'a2', px, 0, 64, 0, 1, height);
fillStyle('a');
fillRect(px - 3, py - 32, 69, 284);
draw(widePipeTexture, px - 1, py - 30, 66, 0, 1, 28);
draw(widePipeTexture, px - 1, py + 222, 66, 0, 1, 28);
fillStyle('i');
fillRect(px - 3, py, 69, 220);
...
})
В памяти содержится информация только о двух трубах, потому что больше на экран просто не влезет. Игра спроектирована так, что при уходе трубы с индексом равным 0 за экран, она удаляется, а игрок получает очко:
if (px < -64) {
score++;
bestScore = score > bestScore ? score : bestScore; // тернарный оператор короче, чем Math.max
pipes = [...pipes.slice(1), [pipes[1][0] + 284, gate()]];
}
Информация от трубах хранится в виде массива кортежей [x,y]
, где x
- расстояние от левого края canvas
до начала трубы, y
- расстояние от верхнего края экрана до конца верхней трубы (затем идет разрыв, куда должна пролететь птица, а потом начинается нижняя труба). y для каждой трубы генерируется в момент создания функцией gate
:
let gate = () => (Math.random() * 292 + 64) | 0;
Расстояния рассчитаны таким образом, чтобы игрок не мог врезаться в трубу с индексом, отличным от 0, поэтому и проверка на столкновение проводится только для одной трубы:
let [[px, py]] = pipes; // берем координаты только первой трубы
if ((px < 198 && px > 98 && (y < py || py + 188 < y)) || (y < 0 || y > 616)) {
// game over
setup();
}
Кстати, как вы уже заметили, все константы заранее посчитаны, потому что так код занимает меньше места.
После труб рисуется птица. У нее всегда один и тот же x
, а вот y
постоянно изменяется:
let gravityAcceleration = .5;
let flyUpSpeed = -12;
vSpeed += gravityAcceleration;
y += vSpeed;
...
draw(birdTexture, 164, y, 16, 5);
// над птицей рисуем анимированные крылья
let wingsPhase = ((tick / 4) | 0) % 4;
wingsPhase % 2
? draw(wing2Texture, 162, y + 10, 7, 1)
: !wingsPhase
? draw(wing1Texture, 162, y + 6, 7, 1)
: draw(wing3Texture, 162, y + 12, 7, 1);
...
c.onclick = () => {
// взлет вверх при клике
vSpeed = flyUpSpeed;
}
Ну и в конце рисуем текст, чтобы сказать игроку во что он вообще играет, сколько очков набрал и показать экран проигрыша:
fillStyle('b');
fillText(`Score: ${score} | Best: ${bestScore}`, 26);
if (!playing) {
fillText(played ? 'Game over' : 'Flappy bird', 210, 32);
fillText('Click to play' + (played ? ' again' : ''), 430);
!played && fillText('QR code edition', 230, 12, 246);
}
Мы рассмотрели почти весь код, однако, кое что я все же опустил, но вы можете найти полный код проекта и даже поиграться самостоятельно на странице проекта на Github.
Сборка
После того как наш код дописан можно приступить к сборке. Благодаря тому, что мы позаботились об инструментах заранее нам нужно всего лишь запустить:
npm i && npm run build
Скрипт сам создаст папку output и поместит туда результат, а именно:
- Минифицированный HTML;
- Data URL;
- PNG-картинку с QR кодом.
А также, выведет детализацию по размерам:
Original size is 3.82 KB (3907 bytes)
Minified size is 1.53 KB (1569 bytes)
b64 size is 2.06 KB (2114 bytes)
Done!
А вы можете прямо сейчас отсканировать QR код:
или сразу скопировать data-url в свой бразуер:
data:text/html;base64,PGh0bWw+PGhlYWQ+PHRpdGxlPkZsYXBweSBCaXJkPC90aXRsZT48bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLGluaXRpYWwtc2NhbGU9MSxtYXhpbXVtLXNjYWxlPTEsdXNlci1zY2FsYWJsZT0wIj48L2hlYWQ+PGJvZHkgc3R5bGU9ImJhY2tncm91bmQ6IzAwMCI+PGNhbnZhcyBzdHlsZT0iaGVpZ2h0Ojk2JTtwb3NpdGlvbjpmaXhlZDt0b3A6NTAlO2xlZnQ6NTAlO3RyYW5zZm9ybTp0cmFuc2xhdGUzZCgtNTAlLC01MCUsMCkiIGlkPSJjIiB3aWR0aD0iMzYwIiBoZWlnaHQ9IjY0MCI+PC9jYW52YXM+PHNjcmlwdD5sZXQgaT1jLmdldENvbnRleHQoIjJkIiksdT0oaS50ZXh0QWxpZ249ImNlbnRlciIsYy5oZWlnaHQpLGU9e2E6IjAwMCIsYjoiZmZmIixjOiJmNzEiLGQ6ImYyMCIsZToiZWVjIixmOiIxNzAiLGc6IjFhMCIsaDoiMWQwIixpOiIwYWMifSxnPWkuZmlsbFJlY3QuYmluZChpKSx0PShhLGUsYz0xNix1PTE4MCk9PntpLmZvbnQ9YysicHggY291cmllciIsaS5maWxsVGV4dChhLHUsZSl9LG49YT0+aS5maWxsU3R5bGU9IiMiK2VbYV0sbD0iYTZ1OGEyYzNhYjJhdTZhYzRhYjRhdTRhYzVhYjNhYmF1MmFjNmFiM2FiYXUyYWM3YWI0YXUyYWM4YTZ1YWM3YWQ2YXVhYzVhZGE2dTJhYzZhZDVhdTNhMmM1YTV1NmE1IixmPSJhNHUyYWU0YXVhZTVhMmU1YXVhZTNhdTNhMyIscj0iYTV1YWU1YTJlNWF1YTV1IixvPSJhNXVhZTVhMmU0YXVhZTNhdTNhNCIsYj0iZzZoNGc0aDloM2c0aDRnOWc0ZjlmMyIsZD0iZzMiK2IrImYzIixoLG0scCxzLHYseD0wLHk9MCxrPTQsQT0uNSxhPS0xMixDPTAsRj0oKT0+MjkyKk1hdGgucmFuZG9tKCkrNjR8MCxSPShhLGUsYyx1LGk9MCx0PTIsbD0yKT0+e2Zvcig7YS5sZW5ndGg7KXt2YXIgZj1hWzBdLHI9K2FbMV07aWYobihmKSxhPWEuc2xpY2UociE9cj8xOjIpLCJ1Ij09PWYpaSs9cnx8MTtlbHNlIGZvcih2YXIgbz0ocnx8MSkraTtpPG87aSsrKWcoZStpJXUqdCxjKyhpL3V8MCkqdCx0LGwpfX0sUz0oKT0+e209YSxwPXY9MCxoPTMwOCxzPVtbNTA0LEYoKV0sWzc4OCxGKCldXX0scT0oKT0+e24oImkiKSxnKDAsMCwzNjAsNjQwKSx2JiYobSs9QSxoKz1tLHMubWFwKGE9PnthWzBdLT1rO3ZhclthLGVdPWE7UigiYTIiK2IrImEyIixhLDAsNjQsMCwxLHUpLG4oImEiKSxnKGEtMyxlLTMyLDY5LDI4NCksUihkLGEtMSxlLTMwLDY2LDAsMSwyOCksUihkLGEtMSxlKzIyMiw2NiwwLDEsMjgpLG4oImkiKSxnKGEtMyxlLDY5LDIyMCksYTwtNjQmJihwKyssQz1wPkM/cDpDLHM9Wy4uLnMuc2xpY2UoMSksW3NbMV1bMF0rMjg0LEYoKV1dKX0pLFtbZSxhXV09cyxlPDE5OCYmOTg8ZSYmKGg8YXx8YSsxODg8aCl8fGg8MHx8NjE2PGgpJiZTKCksUihsLDE2NCxoLDE2LDUpO3ZhciBhLGU9KHkvNHwwKSU0O2UlMj9SKHIsMTYyLGgrMTAsNywxKTplP1IobywxNjIsaCsxMiw3LDEpOlIoZiwxNjIsaCs2LDcsMSksbigiYiIpLHQoYFNjb3JlOiAke3B9IHwgQmVzdDogYCtDLDI2KSx2fHwodCh4PyJHYW1lIG92ZXIiOiJGbGFwcHkgYmlyZCIsMjEwLDMyKSx0KCJDbGljayB0byBwbGF5IisoeD8iIGFnYWluIjoiIiksNDMwKSx4KXx8dCgiUVIgY29kZSBlZGl0aW9uIiwyMzAsMTIsMjQ2KSx5KysscmVxdWVzdEFuaW1hdGlvbkZyYW1lKHEpfTtTKCkscSgpLGMub25jbGljaz0oKT0+e209YSx2PXg9MX08L3NjcmlwdD48L2JvZHk+PC9odG1sPg==
И поиграть в Flappy Bird QR code edititon! А самое крутое это то, что для игры не нужно ничего кроме QR кода (ну и устройства с браузером, конечно).
Также, приложу скриншот с игрой. Однако, я рекомендую Вам попробовать поиграть самим!
Спасибо за внимание!
⚠️ Увы, но стандартная камера айфона не понимает data-url. Она видит QR код, но говорит “no usable data found” 😟. У меня получилось распознать код первым приложением из App Store, а вообще и с любым другим приложением проблем быть не должно.
🤓 В самом начале мы обсуждали версии QR кодов. Так вот, у нас получился QR код 39 версии (предпоследней).
Ссылки
- qrcode, просмотрено 12 января 2023 <https://www.npmjs.com/package/qrcode>
- QR Code Specification, просмотрено 12 января 2023, <https://www.labeljoy.com/qr-code/qr-code-specification/>
- QR code, просмотрено 12 января 2023, <https://en.wikipedia.org/wiki/QR_code>
- Data URLS, MDN, просмотрено 12 января 2023, <https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs>
- Base64 Characters, просмотрено 12 января 2023, <https://base64.guru/learn/base64-characters>