Как мы отмечали 256 день года и рисовали пиксели через API

13 сентября в Контуре отмечали День программиста. В самом большом офисе разработки играли в Pac-Man и пытались съесть 280 коробок с пиццей. Одновременно полторы тысячи человек рисовали пиксели в онлайне. В этом посте четыре разработчика рассказывают, как делали праздник.


79b2ccd3fe4c4fd0ab180aadd8bbf6ea.png


Часть 1. Рассказывает Игорь green_hippo, который стырил идею на Reddit


День программиста у нас отмечает вся компания, а не только разработчики. Поэтому была нужна идея для онлайновой игры, в которой могут участвовать все желающие. Я вспомнил, что в апреле прошёл Reddit Place — социальный эксперимент по коллективному рисованию на холсте 1000×1000 пикселей, в котором участвовал миллион человек.


Я решил, что надо сделать свой Place, с таймлапсом и API.



На Reddit миллион человек рисовал на холсте размером один мегапиксель. Каждый мог закрасить не больше одного пикселя раз в 5–20 минут. Если сделать праздничный холст 256×256 пикселей (в 15 раз меньше) и учесть, что у нас не миллион сотрудников (а в 200 раз меньше), то задержку между пикселями тоже должна быть примерно в 10 раз меньше.


Поэтому для нашего поля 256×256 пикселей я выбрал задержку от 2:56 до 0:32. А после этого рассказал об идее коллегам, которые согласились помочь.


Часть 2. Рассказывает Вероника aminopyrodin, которая поборола себя и тормозной canvas


Я сразу поняла, что на фронте будет нужен холст, палитра и зум. Но дизайнеры (Владимир dzekh и Юлия krasilnikovayu) оказались хитрее и придумали ещё перемотку, статистику, лидерборд и скриншоты.


4eb5e6cc937e4940aa72e16b5986fff9.jpg


Кстати, сначала в палитре было меньше цветов, но потом ребята добавили коричневый, чтобы не ограничивать ничьи творческие порывы.


Тем временем я, как современный фронтендер, рефлекторно начала думать о том, чтобы настроить Webpack, Babel и Autoprefixer. А когда очнулась, узнала, что бэкенд-разработчик уже всё сделал. И оно даже работало. Криво-косо, но работало: точки на canvas ставились, зум зумился. Я отпилила от прекрасного дизайна все ненужное и красивенько сверстала.


Остались две проблемы: Edge и Safari.


313cf57e3a694b0eb5ad673550a38e9e.png


В Safari и правда все тормозило со страшной силой. Сначала обнаружила, что canvas не вынесен в отдельный композитный слой. Поэтому браузер при каждом обновлении холста перерисовывал весь документ. Добавила канвасу transition: translateZ(0), и все стало тормозить быстрее. Потом отрефакторила остальной бакендерский код, избавилась ещё от десятка перерисовок. Интерфейс полетел на первой космической.


Об IE я сразу не заботилась, потому что знала, что игроки будут пользоваться нормальными браузерами. Беда пришла от старшего брата. Если просишь Edge нарисовать квадрат, он категорически отказывается. Говорит: «Но плавные переходы лучше!» — и размывает весь рисунок.


c86e204dec9b4042beb71fc640b16e92.png


Такая же проблема была у ребят из Reddit. Сначала я решила её с помощью CSS-свойства image-rendering и флага CanvasRenderingContext2D.imageSmoothingEnabled. Но перед запуском оказалось, что Edge косячит при общении с сервером через вебсокеты. Поэтому я и его объявила ненормальным браузером.


Горжусь, что трижды пыталась принести в код React, Webpack, Babel, LESS и Autoprefixer, но смогла победить себя. В итоге всё написано на чистом ES6+ и CSS, но с модными гридами, вебсокетами и fetch-ем.


Часть 3. Рассказывает Иван vansel, который попробовал новую классную библиотеку и не рад этому


Я не хотел писать всё с нуля, поэтому поискал готовое. Оригинальный Place лежит на Github, но там слишком много кода. Я взял простой клон под NodeJS и прошёлся по нему напильником. Именно поэтому, когда за дело взялась Вероника, интерфейс уже как-то работал. Вообще, есть уйма клонов, выбирайте для себя любой.


В коде выбранного клона пришлось пофиксить уязвимости и добавить недостающее: таймер, расширенную палитру, перемотку в онлайне, сбор статистики, рисование через вебсокеты вместо REST-запросов, вход через Паспорт (наш внутренний аутентификатор).


599e97883dc24e579db80151649efd3b.png


Архитектура была такая: пользователь ставил пиксель в браузере, браузер отправлял сообщение через вебсокет на сервер, сервер отправлял сообщение об изменении холста в очередь (Apache Kafka). Потом серверы забирали данные из очереди и отправляли всем клиентам. Выше оригинальная схема от автора клона, на которой клиенты ещё общаются с сервером с помощью REST-запросов.


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


Примерно через сутки после начала игры случился инцидент. Я исправил баг и перезапустил сервер. А пользователи увидели, что часть нарисованных точек пропала.


122c798b04d5493fb423304ff92a215a.png


Дело было в том, что при подключении к серверу клиент запрашивал конкретную версию холста. Поэтому после старта сервер взял из кэша не последний снэпшот, а какую-то старую версию, которую запросил один из клиентов. Пользователи не увидели свежие пиксели и начали рисовать по-новой, а сервер продолжил складывать данные в очередь.


Когда я понял, в чём дело, я почистил кэш. Сервер заново загрузил данные из очереди и сгенерировал актуальную версию холста. Получился забавный эффект: новые рисунки пользователей наложились на предыдущие, так как вернулось всё потерянное и добавились новые изменения. Фикс был быстрый, поэтому ничего не испортилось:


9962efb7843945ddb47634873f33ca61.png


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


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


7f5389643b7c4b8fb9ac700da443d22f.png


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


Сервер был под NodeJS, поэтому я выбрал LokiJS. Эту базу хвалили за простоту и скорость работы, потому что все данные хранятся в памяти и автоматически записываются на диск через заданные интервалы времени. Для моей задачи подходило.


Я настроил сохранение раз в 1 минуту. Протестировал локально, в том числе под нагрузкой — всё работало как часы. А на боевой площадке происходило что-то паранормальное. Данные сохранялись на диск не по расписанию, а по собственному желанию. Например, в течение нескольких часов не сохранялись ни разу. За три дня я так и не нашёл причины этого поведения. В итоге, много статистики потерялось при перезапусках сервера.


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


$ ffmpeg -pattern_type glob \
         -i "*.png" \
         -c:v libx264 \
         -vf format=yuv420p \
         timelapse.mp4

$ ffmpeg -i timelapse.mp4 \
         -i sci-fi.mp3 \
         256.mp4


Часть 4. Рассказывает Павел xoposhiy, который загнул радугу и запустил ракету через API


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


7febb3d5a6f54a8bbb50d797fd77b958.png


Я тоже в этом поучаствовал:


93f94c812c9c4a81891eafa51e7a8744.png


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


сетку для безошибочного нанесения картинок

d0e12a55ea74446ea99ec464eb193d27.png


браузерного бота

8a392747fade48f2b123690da7b84fe5.png


Я ждал от Дня программиста большего. И дождался — на второй день Игорь опубликовал в Стаффе такой фрагмент кода и стал раздавать желающим API-ключи:


80814a5c1c3d4d9b9c02a34f6a2ae786.png


Это было уже что-то!


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


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


Можно было нарисовать часы с текущим временем. Или движущуюся картинку, которая ползёт по холсту и затирает чужие рисунки… чтобы потом их восстановить. И тут я придумал сюжет, который объединил эти две идеи.


Часы стали таймером обратного отсчёта, движущаяся картинка — взлетающей ракетой. К тому же, ракету очень удобно рисовать — сначала на пиксель удлиняешь верхнюю часть, потом на пиксель укорачиваешь нижнюю. Это не только хорошо смотрится, но и экономит пиксели, ведь задержку при рисовании через API никто не отменял.


Это должен был быть самый медленный полёт ракеты в истории человечества. С текущей задержкой за пару часов я мог сдвинуть ракету всего на несколько пикселей. Нужно было либо уменьшать ракету, либо двигать её скачками, либо смириться с тем, что лететь она будет сутки. Поделился муками выбора с Игорем, а он со словами «Твори добро!» внезапно отсыпал без малого 50 ключей для API. С таким количеством ключей ракета могла достичь скорости один пиксель в секунду!


ce616d6883d849f6a08c6e98421b16dc.jpg


Осталось немного: выбрать дизайн ракеты и написать весь код. Я отбросил мультяшные ракеты и выбрал ракету-носитель «Восток». Сразу стало понятно, что полёт ракеты должен заканчиваться выводом на орбиту корабля Восток-1.


Почему «Восток»? Потому что прямо сейчас куча инженеров из Контура занимается секретным проектом с кодовым названием Vostok. Я хотел, чтобы парням было приятно.


Я настроил бота, запустил таймер обратного отсчёта, позвал зрителей через Стафф. Ракета взлетела. И тут я понял, как нелепо выглядит ракета в космосе с неотделёнными разгонными блоками и первой ступенью. Чудом нашёл 10 свободных минут, чтобы добавить отделение ступени и перезапустить бота. Так что это был не только самый медленный полет ракеты в истории человечества, но и первый полёт ракеты, в середине которого поменяли её конструкцию.


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


87ea14b58f2d4b6a839fa531ed297068.png
0ab36e40c1924da5a977f75ac5ffd4c6.png


Кстати, без NSFW-контента не обошлось. Кто-то из нарисованного моим первым нескучным ботом слова TRON упорно делал слово PRON.


Были и более интересные рисунки

62e2db1bc48b4ac78f1ada1886eae8ba.jpg


Ваня потом рассказал, что 13 сентября на холсте одновременно рисовало 1630 человек и десяток ботов, то есть примерно треть всех работников компании. В среднем к серверам было подключено 440 клиентов, а в дневные часы — 840.


В итоге у нас получилась такая картинка:


b7268e24dbda46c795e91f9e337e7b6f.png


И такой таймлапс. Моя ракета взлетает на 27 секунде:



А вы программируете по праздникам и для праздников? Расскажите нам в комментариях.


P.S. Если интересно, о чём мы не рассказываем на Хабре, подписывайтесь на наш канал в Телеграме.

© Habrahabr.ru