Логотип блога Владислава Иванова

Привет!

QR коды - очень крутая штука. Но что если я скажу вам, что в них можно еще и поиграть. Интересно? Тогда поехали!

Итак, сегодня я расскажу о том как я написал игру flappy bird и поместил ее в QR код. Задача не такая простая как кажется на первый взгляд. В качестве челленджа мы хотим, чтобы вся игра содержалась внутри QR кода и для того чтобы поиграть нам был нужен только QR код, то есть нам не подходит вариант с ссылкой на игру внутри QR кода. Это неинтересно!

Чаще всего, когда вы сканируете QR код данные из декодера попадают в поисковую строку браузера. Конечно, есть и другие сценарии использования, однако я пока предлагаю остановиться на этом. Значит наша задача заключается в том, чтобы передать исходный код нашей игры с через строку браузера. Но можем ли мы так сделать? Конечно, можем и помогут нам в этом Data URLs. Мы можем проверить работоспособность данного метода просто взяв любой валидный HTML код, закодировав его в base64 и добавив вначале префикс data:text/html;base64,. Далее нам нужно просто получившуюся строку закодировать в QR код. Звучит довольно просто, но здесь есть одно не очень приятное обстоятельство: мы не можем закодировать в QR код бесконечное количество данных.

Ограничения

По спецификации QR коды делятся на версии. Номера версий варьируются от 1 до 40. Каждая версия имеет особенности в конфигурации и количестве точек(модулей) составляющих QR-код. Версия 1 содержит 21×21 модулей, версия 40 — 177×177. От версии к версии размер кода увеличивается на 4 модуля на сторону.

Версии QR кодов

Каждой версии соответствует определенная емкость с учетом уровня коррекции ошибок. Чем больше информации необходимо закодировать и чем больший уровень избыточности используется, тем большая версия кода нам потребуется.

QR код поддерживает несколько режимов кодирования:

ModeCharactersCompression
Числовой0, 1, 2, 3, 4, 5, 6, 7, 8, 93 символа представлены 10 битами
Буквенно-цифровой0–9, A–Z (только верхний регистр), пробел, $, %, *, +, -, ., /, :2 символа представлены 11 битами
БайтСимволы из набора ISO/IEC 8859-1Каждый символ представлен 8 битами
КандзиСимволы из системы Shift JIS, основанной на JIS X 02082 кандзи символа представлены 13 битами

Также, QR-коды имеют специальный механизм увеличения надежности хранения зашифрованной информации. Для кодов созданных с самым высоким уровнем надежности могут быть испорчены или затерты до 30% поверхности, но они сохранят информацию и будут корректно прочитаны. Для исправления ошибок используется алгоритм Рида-Соломона.

В таблице ниже показано максимальное количество сохраняемых символов в каждом режиме кодирования и для каждого уровня исправления ошибок:

ModeL (~7%)M (~15%)Q (~25%)H (~30%)
Числовой7089559639933057
Буквенно-цифровой4296339124201852
Байт2953233116631273
Кандзи181714351024784

Алфавит 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:

javascript
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 документ:

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Flappy bird</title>
  </head>
  <body>
  </body>
</html>

Я хотел бы чтобы в игре были как можно более адекватные текстуры, однако мы договорились не загружать ничего дополнительно, поэтому придется программировать рисование текстур. Можно было бы развлекаться с div, box-shadow, различными градиентами или SVG, но все эти подходы достаточно многословны и особо нет эффективных способов минифицировать такой код. Поэтому я решил выбрать canvas:

html
...
  <body>
    <canvas id="c" width="360" height="640"></canvas>
  </body>
...

Я создал DOM элемент и установил атрибут id.

Теперь в коде я могу просто обратиться к нему как window.c или просто по имени c и нет никакой необходимости в использовании методов поиска DOM элементов в документе по типу querySelector или getElementById, имена которых на самом деле достаточно длинные и не поддаются сокращению. Кстати, хотел бы обратить внимание на этот момент. Мы будем использовать canvas, то есть для рисования мы будем использовать различные методы CanvasRenderingContext2D, и есть смысл присваивать эти методы в переменные, потому что минифицировать свойства объекта мы не можем, однако для присваивания нам необходимо вызвать bind , чтобы передать this контекст, коим в данном случае является контекст рендеринга canvas. Вот о чем я говорю:

javascript
let ctx = c.getContext('2d');

ctx.fillRect(...)
ctx.fillRect(...)
ctx.fillRect(...)

После минификации даст что-то типа такого (проблельные символы оставлены для понятности):

javascript
let a = c.getContext('2d');

a.fillRect(...)
a.fillRect(...)
a.fillRect(...)

В то время как такой кусок кода:

javascript
let ctx = c.getContext('2d');

let fillRect = ctx.fillRect.bind(ctx);

fillRect(...)
fillRect(...)
fillRect(...)

Превратится во что-то типа такого:

javascript
let a = c.getContext('2d'), b = a.fillRect.bind(a);

b(...)
b(...)
b(...)

То есть профит от такого присвоения есть, но не всегда. Для каждого отдельного случая нужно считать длину строк. Например, для метода fillRect такое присвоение имеет смысл только если этот метод вызывается в коде 2 или более раз.

Конечно, есть и другие способы присвоения функции, например:

javascript
let fillRect = (...args) => ctx.fillRect(...args);

Но такой вариант будет длиннее после минификации по сравнению с кодом, где используется bind. Поэтому я решил использовать код с bind, так как не смог найти вариант, который был бы короче.

Кстати, повсеместно в коде используется let , потому что он на 2 символа короче const 🙂.

Текстуры

Я уже говорил о том, что хотел бы использовать как можно более похожие на оригинал текстуры для своей игры. Например, примерно такую птицу я бы хотел видеть в игре:

Текстура птицы

При этом у нас нет возможности загрузить картинки через интернет, поэтому я посчитал, что самым коротким способом будет хранить изображения в переменных типа string прямо в исходном коде. Но как уместить картинки в 1.6 килобайта с учетом того, что есть еще и код игры?

Можно заметить, что для того, чтобы нарисовать текстуру выше нам нужно всего 5 цветов. В то время как популярные форматы изображений такие как JPEG или PNG позволяют кодировать миллионы цветов, что достигается достаточно высокой разрядностью цвета. Например, цветные изображения JPEG хранят 24 бита на пиксель, а PNG до 48 бит. Поэтому вариант с тем чтобы просто закодировать картинку в base64 сразу отпал.

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

text
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% для ЛЮБОГО набора данных. Каждому символу необходимо присвоить код. И в результирующую строку записывать не сам символ, а результат выражения:

javascript
char((code(color1) << 4) | code(color2))
// char - получения символа по его коду
// code - получение кода по символу

Тогда в 8 битах мы будем хранить информацию о 2 пикселях. Но есть один нюанс. Коды символам нужно присваивать с 2 (0b10). Почему? В таблице ASCII символов до кода 32 идут служебные символы. Я не нашел способа их напечатать на Mac, а также не уверен относительно того можно ли использовать их в скрипте. Скорее всего, можно, но потребуется экранирование. В общем, для решения этой проблемы достаточно добавить смещение 2 и тогда в результате функции code будет всегда получаться число больше 32, а значит и печатаемый символ.

В результате с помощью первого алгоритма (RLE с оптимизацией единичных символов) я получил:

text
u6a6u9a2c3ab2au7ac4ab4au5ac5ab3abau3ac6ab3abau3a5c3ab4au2ad5ac3a6uad5ac2ae6aua5c2aea6u3ac6ae5au4a2c5a5u7a5u7

А в результате второго:

text
ÌÌÌ333ÌÌÌÌÃ5U4CÌÌÌÃUU4DCÌÌÃUUSDCCÌÃUUU4D4<Ì335U4DCÌ6ff5U333ÃffcU7wwsÃ33U7333ÌÃUUU7ww<ÌÃ5UU33<ÌÌÌ33<ÌÌÌ

И разница составила всего 6 байт. Конечно, результат второго алгоритма можно попробовать еще прогнать через RLE, но надо помнить о том, что раскодировать это все тоже придется кодом скрипта, который тоже занимает какое то место и я решил, что RLE меня вполне устраивает.

Кстати, можно было бы попробовать закодировать исходную строку алгоритмом Хаффмана, и даже заранее вычислить кодовое дерево, но в таком случае довольно трудно контроллировать непоявление служебных непечатаемых символов, а код реализации декодера достаточно громоздкий по сравнению с кодом декодера RLE или битовых последовательностей.

Таким образом, в игре появились текстуры:

javascript
let birdTexture = `a6u8a2c3ab2au6ac4ab4au4ac5ab3abau2ac6ab3abau2ac7ab4au2ac8a6uac7ad6auac5ada6u2ac6ad5au3a2c5a5u6a5`;
let wing1Texture = `a4u2ae4auae5a2e5auae3au3a3`;
let wing2Texture = `a5uae5a2e5aua5u`;
let wing3Texture = `a5uae5a2e4auae3au3a4`;
let pipeTexture = 'g6h4g4h9h3g4h4g9g4f9f3';
let widePipeTexture = 'g3' + pipeTexture + 'f3';

После минификации этот код будет весить примерно 220 байт, что вполне приемлемо.

Но пока что это всего лишь обычные строки. Для их превращения в изображения тоже нужно проделать определенные действия:

javascript
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. Сделано для быстродействия.

Код

Вся игра у нас (как и любая другая, насколько мне известно) будет крутиться в бесконечном цикле:

javascript
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 (цвета используем трехбуквенные). Я думал насчет генерации фона, но не придумал как уместить этот код в наш размер, поэтому просто залил. Далее идет рисование труб:

javascript
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 за экран, она удаляется, а игрок получает очко:

javascript
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:

javascript
let gate = () => (Math.random() * 292 + 64) | 0;

Расстояния рассчитаны таким образом, чтобы игрок не мог врезаться в трубу с индексом, отличным от 0, поэтому и проверка на столкновение проводится только для одной трубы:

javascript
let [[px, py]] = pipes; // берем координаты только первой трубы

if ((px < 198 && px > 98 && (y < py || py + 188 < y)) || (y < 0 || y > 616)) {
  // game over
  setup();
}

Кстати, как вы уже заметили, все константы заранее посчитаны, потому что так код занимает меньше места.

После труб рисуется птица. У нее всегда один и тот же x, а вот y постоянно изменяется:

javascript
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;
}

Ну и в конце рисуем текст, чтобы сказать игроку во что он вообще играет, сколько очков набрал и показать экран проигрыша:

javascript
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.

Сборка

После того как наш код дописан можно приступить к сборке. Благодаря тому, что мы позаботились об инструментах заранее нам нужно всего лишь запустить:

bash
npm i && npm run build

Скрипт сам создаст папку output и поместит туда результат, а именно:

  1. Минифицированный HTML;
  2. Data URL;
  3. PNG-картинку с QR кодом.

А также, выведет детализацию по размерам:

text
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 в свой бразуер:

text
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 версии (предпоследней).

Ссылки

  1. qrcode, просмотрено 12 января 2023 <https://www.npmjs.com/package/qrcode>
  2. QR Code Specification, просмотрено 12 января 2023, <https://www.labeljoy.com/qr-code/qr-code-specification/>
  3. QR code, просмотрено 12 января 2023, <https://en.wikipedia.org/wiki/QR_code>
  4. Data URLS, MDN, просмотрено 12 января 2023, <https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs>
  5. Base64 Characters, просмотрено 12 января 2023, <https://base64.guru/learn/base64-characters> 
Поддерживается markdown

Пока нет комментариев