Что вы делаете с незакрытыми вкладками браузера? На мой взгляд, они занимают слишком много места на экране, поэтому на этой неделе я разобрался, как запустить в этих вкладках Pong.
Это 240 вкладок браузера в плотной сетке 8x30. И в них запущен Pong! Видно, что мяч и ракетки перемещаются по canvas в окне и во всех вкладках.
Код (ужасный) можно посмотреть на GitHub. Но как он работает?
Источник вдохновения
Источником вдохновения для моего проекта стал мой друг Tru, создавший на прошлой неделе версию Flappy Bird, работающую в favicon (Flappy Favi).
FlappyFavi великолепна, но понять происходящее в ней довольно сложно, ведь фавиконки очень маленькие. Я подумал, что смогу решить эту проблему.
Лучшее, что я придумал — отрисовывать изображение в нескольких вкладках. При этом возникло несколько проблем:
Как создать красивую сетку из вкладок для рисования на них?
Как обновлять состояние этих вкладок, даже если они находятся в фоновом режиме?
Как координировать вкладки?
Прототипирование
Первая задача заключалась в поиске способов создания сетки вкладок. Я начал с того, что открыл окно Chrome и щёлкал по кнопке новой вкладки, пока вкладки не стали очень маленькими. Это выглядело примерно так:

Начало многообещающее! Мы получили красивую сетку. А если открыть второе окно и правильно его расположить, то получится вторая строка.
Но мне нужна была большая сетка. Создавать её вручную было бы очень трудоёмко, поэтому я обратился к одному из своих любимых инструментов: AppleScript.
AppleScript — это мощный и причудливый инструмент для управления программами на Mac; в нём можно писать практически на английском, но он достаточно многословен и строг, поэтому по большей мере вы будете писать на Python с кучей дополнительных слов.
Но здесь он подходит идеально. Я написал скрипт, открывающий 8 окон Chrome по 30 вкладок в каждом и тщательно выравнивающий каждое окно одно под другим.
Возникла пара раздражающих проблем, например, Chrome пытается повторно открыть закрытые вкладки, поэтому в начале скрипт должен очистить их.
Но в конечном итоге код оказался относительно простым. Его ядро выглядит так:
-- Задаём границы окон (x, y, width, height)
set bounds of newWindow to {x, y, x + width, y + height}
global tabCount
set tabCount to 0
tell newWindow
set URL of active tab to baseURL & "windowIndex=" & windowNum & "&tabIndex=" & tabCount
end tell
set tabCount to (tabCount + 1)
-- Создаём указанное количество вкладок
repeat (numTabs - 1) times
tell newWindow
if windowNum is (maxWindows - 1) and tabCount is (numTabs - 1) then
make new tab with properties {URL:baseUrl & "windowIndex=" & windowNum & "&tabIndex=" & tabCount & "&isMain=true&numWindows=" & maxWindows & "&numTabs=" & numTabs & "&fullWidth=" & fullWidth}
else
make new tab with properties {URL:baseURL & "windowIndex=" & windowNum & "&tabIndex=" & tabCount}
end if
end tell
set tabCount to (tabCount + 1)
end repeat
Быстрые обновления фавиконок
Следующая проблема связана с обновлением фавиконок.
По умолчанию браузеры ищут фавиконки по нескольким известным URL. Но можно и добавить в head HTML-разметки элемент, говорящий «моя фавиконка находится здесь».
При обновлении этого элемента браузер заменит иконку. Именно так работает FlappyFavi. Похоже, Сhrome обновляет иконку примерно четыре раза в секунду.
[Не знаю, как это делают другие браузеры. В частности, Firefox позволяет загружать анимированные фавиконки, что мне очень бы помогло. Но Firefox не позволял мне создать сетку из достаточно мелких вкладок, чтобы в ней было удобно рисовать, поэтому я остался с Chrome. Но мне бы очень хотелось поэкспериментировать с анимациями!]
Но мне было непонятно, как это будет работать, когда вкладка находится в фоне. Для повышения производительности браузеры ограничивают ресурсы, к которым имеет доступ вкладка в фоновом режиме, а большинство наших вкладок не будет на переднем плане!
Я провёл простое тестирование с крошечным циклом, обновлявшим фавиконку каждые 250 мс. И, разумеется, этот цикл работал для фоновой вкладки всего примерно раз в секунду!
Браузер выполнял троттлинг моего цикла setInterval! Я начал придумывать способы обхода этой проблемы.
Первым делом я решил использовать в своих целях web audio API — я знал, что у них есть хорошая поддержка аудио, продолжающего воспроизводиться в фоне, и что можно добавить некие обратные вызовы в код аудио; я попробовал воспроизводить беззвучный сигнал и поместить мой код в поток (thread) аудио, чтобы проверить, поможет ли это. Но эта система не заработала.
Поэтому я попробовал веб-воркеров. Веб-воркеры — это способ перенести вычислительно затратную задачу с основного потока браузера (чтобы она не препятствовала рендерингу). И я подумал, что она может подвергаться меньшему троттлингу.
Я переместил таймер в веб-воркер и заставил его при срабатывании таймера передавать post-сообщения в основной документ. И это замечательно сработало!
Код здесь немного длинный, но относительно простой. У нас есть воркер, циклически обходящий эмодзи и возвращающий их в URL данных, которые мы передаём основной вкладке, обновляющей фавиконку.
Код фавиконок веб-воркера
// воркер
let intervalId = null;
let counter = 0;
const emojis = [" 