Pong rodando em 240 abas do navegador
(eieio.games)- Um experimento de Pong foi implementado usando o espaço vazio da barra de abas que não foram fechadas como se fosse uma tela, com os favicons de 240 abas do Chrome servindo como pixels
- A grade de abas 8×30 foi criada com AppleScript, e cada aba calcula sua própria posição com
windowIndexetabIndexda query da URL - O
setIntervalem abas em segundo plano ficou lento, rodando em intervalos de cerca de 1 segundo, mas ao mover o timer para um Web Worker, a atualização dos favicons passou a ser rápida o suficiente - Em vez de WebSocket no servidor, foi usada a Broadcast Channel API para sincronizar abas do mesmo domínio, e a aba principal só inicia a animação depois que todas as abas terminam de se registrar
- O maior gargalo era recriar centenas de favicons a cada frame, e ao atualizar apenas as abas que realmente mudavam, a animação ficou muito mais suave
Como usar 240 abas como se fossem uma única tela
- O projeto é um experimento para rodar Pong aproveitando o espaço ocupado por abas do navegador que não foram fechadas
- A tela é composta por uma grade de abas 8×30, formada por 8 janelas do Chrome com 30 abas em cada uma
- A bola e as raquetes se movem conectando o
canvasHTML da janela em primeiro plano com a área de favicon da barra de abas acima - O código está disponível no GitHub em faviconic, com a observação de que é um código “awful” em estado de protótipo
A ideia começou com Flappy Favi
- A inspiração veio de Flappy Favi, uma versão de Flappy Bird rodando dentro de um favicon criada por Tru
- O favicon é a pequena imagem mostrada na barra de abas do navegador, mas sozinho é pequeno demais para enxergar bem
- Para desenhar uma imagem distribuída por várias abas, primeiro era preciso resolver três problemas
- Como organizar as abas em uma grade visualmente boa
- Como atualizar os favicons rapidamente mesmo em abas em segundo plano
- Como sincronizar a tela entre várias abas
Criando uma grade de abas do Chrome com AppleScript
- No início, a viabilidade foi testada repetindo cliques no botão de nova aba do Chrome para deixar as abas menores e alinhando uma segunda janela para formar uma segunda linha
- Como montar manualmente uma grade grande era trabalhoso, isso foi automatizado com AppleScript
- O script abre 8 janelas do Chrome, cria 30 abas em cada uma e posiciona as janelas para que fiquem empilhadas umas sobre as outras
- Como o Chrome tentava reabrir abas fechadas, foi preciso limpar isso no início do script
- Cada URL de aba inclui
windowIndexetabIndex- A última aba aberta recebe parâmetros adicionais como
isMain=true,numWindows,numTabsefullWidth
- A última aba aberta recebe parâmetros adicionais como
Atualizando favicons rapidamente em abas em segundo plano
- O navegador procura a URL padrão do favicon, mas é possível trocar o ícone adicionando ao
headdo HTML um elemento que indica sua localização e atualizando esse valor - A atualização de favicon no Chrome parece ficar em algo como 4 vezes por segundo
- Em abas em segundo plano, o uso de recursos é limitado, então até um pequeno loop trocando o favicon a cada 250 ms acabava rodando só cerca de uma vez por segundo
- Houve uma tentativa de usar callbacks de áudio em segundo plano da Web Audio API como desvio, mas isso não funcionou corretamente
- Depois, o timer foi movido para um Web Worker, e o problema foi resolvido quando o worker passou a enviar mensagens para o documento principal atualizar o favicon
- Na estrutura de exemplo, o worker desenha um emoji em
OffscreenCanvas, converte paradata URLe envia isso ao documento principal- O documento principal troca o
hrefdelink rel="shortcut icon"para atualizar de fato o favicon
- O documento principal troca o
Registro e sincronização das abas com Broadcast Channel
- A comunicação entre abas tinha dois desafios
- Cada aba precisava saber de qual janela e de qual posição de aba ela era
- Era preciso definir qual canal de comunicação transmitiria as atualizações entre as abas
- A identificação da posição foi resolvida lendo
windowIndexetabIndexdos parâmetros de query da URL inseridos pelo AppleScript - A prova de conceito inicial tinha cada aba se conectando a um servidor WebSocket, que enviava a imagem
- A abordagem com WebSocket não combinava muito com o objetivo do projeto
- Havia um conflito estético com a ideia de distribuir isso para abas de navegador no mundo todo sem servidor
- Como cada aba conectava em um momento diferente, as atualizações não ficavam sincronizadas
- Depois, o projeto migrou para a Broadcast Channel API, que transmite informações entre várias abas do mesmo domínio
- Abas em segundo plano enviam uma mensagem
registrationpelo broadcast channel com seus índices de aba/janela, e a aba principal responde comack - A aba principal só inicia a animação depois de receber o registro de todas as abas em segundo plano
Alinhando o canvas com os favicons
- Para fazer parecer que os objetos desenhados na janela em primeiro plano “entram” na barra de abas, o
canvasreal e os favicons acima foram tratados como um únicocanvasexpandido - A implementação exigiu medir manualmente as dimensões reais da janela do Chrome
- 92 pixels da esquerda da janela do Chrome até o primeiro favicon
- 58 pixels da parte inferior do favicon até a parte superior da janela real
- Tamanho do favicon de 16×16
- Com essas medidas e a quantidade de abas/janelas, foram calculados três valores
- A largura do
canvasa ser exibido para alinhar com os favicons - A largura de cada aba
- A largura e a altura do “canvas” total, incluindo a linha de favicons, a barra de URL e o
canvasreal
- A largura do
- Se um retângulo estiver abaixo da barra de URL, ele é desenhado no
canvasreal; se estiver acima da barra de URL, as informações dessa área são transmitidas para outras abas - Cada aba calcula então suas coordenadas de pixel e atualiza o favicon como pixels brancos e pretos
O gargalo estava na geração do favicon, não na transmissão de dados
- A implementação inicial funcionava, mas a animação do
canvastravava, e nem desenhar dentro derequestAnimationFramedeixava o movimento suave - No começo, suspeitou-se que a main thread estivesse copiando dados demais ao calcular o estado de todos os pixels de favicon e colocá-los no broadcast channel para centenas de abas lerem
- A estratégia foi mudada para transmitir apenas a posição dos retângulos e deixar cada aba calcular por conta própria se havia interseção, mas o desempenho não melhorou
- O problema real era que cada aba recriava em todos os frames uma imagem de favicon preto e branco 4×4 e gerava uma URL com
toDataURL("image/png") - Mesmo quando o
canvasresultante não mudava, centenas de abas continuavam regenerando pequenos favicons brancos várias vezes por segundo - Quando a atualização passou a ocorrer apenas se o favicon realmente mudasse, o desempenho melhorou bastante
- Ainda assim, continuou sem ficar claro por que o trabalho das outras abas deixava a animação da aba em primeiro plano tão lenta
Por que escolher Pong em vez de Snake
- Depois do experimento com movimentação de retângulos, foi organizado um pequeno “motor” e escolhido o jogo que seria feito sobre ele
- A primeira candidata foi Snake, já que seu estilo baseado em blocos parecia combinar bem com favicons
- Mas concluiu-se que Snake era baseado demais em blocos, então o efeito de transformar o movimento contínuo do
canvasem pixels discretos na barra de abas acabava menos interessante - No fim, a escolha foi Pong
- A bola e as raquetes precisam cruzar com frequência entre o
canvase a barra de abas - Esse movimento combinava bem com o efeito visual do projeto
- A bola e as raquetes precisam cruzar com frequência entre o
Notas sobre a implementação de Pong
- Depois que a API de desenho de favicon ficou pronta, implementar Pong em si foi relativamente simples
- O jogador controlado pelo computador à direita tenta sempre alinhar o centro da raquete com o centro da bola
- O ângulo da bola ao quicar na raquete é calculado com trigonometria simples usando a posição relativa ao centro da raquete
- Esse método não é realista, mas evita o problema de a bola manter sempre o mesmo ângulo
- Para a detecção de colisão, foi usada uma função que verifica se dois retângulos se intersectam
- Para enfatizar o efeito de bola e raquetes entrando suavemente nos favicons, foi adicionado um trail à bola
O quarto jogo feito no Recurse Center
- O projeto foi criado durante um batch do Recurse Center
- O Recurse Center é descrito como uma espécie de retiro gratuito para programadores, no estilo writer’s retreat
- Este foi o 4º jogo feito ao longo de cerca de 40 dias
- Leitores interessados são incentivados a considerar uma inscrição no Recurse Center
1 comentários
Comentários no Hacker News
Olá! Fui eu que fiz isso. Fiquei curioso para ver se isso pegaria entre os leitores do HN :)
Se tiverem perguntas, eu respondo. Anotando alguns pensamentos soltos: estou muito curioso para saber como ficaria usando favicons animados. O Firefox oferece suporte a favicons animados, então, por exemplo, se eu previsse a posição futura da bola e criasse um SVG animado, acho que daria para obter uma taxa de quadros muito melhor.
Um amigo me disse offline que a rasterização de canvas normalmente é feita na GPU, então minha intuição de desempenho ao ver a animação engasgando parece estar bem errada.
Tenho só uma certa confiança de que o Chrome limita atualizações de favicon a 4 vezes por segundo. Como há várias maneiras de atualizar favicons, talvez eu tenha deixado passar algo
[0] https://jakesgordon.com/writing/javascript-pong/part1/
Depois do 1 Million Checkboxes, você fez isto, global cap locks, regex de bad apple, uuid etc.; o embalo é impressionante
Vi a apresentação recente do Nolen na Recurse, e esses jogos pontuais totalmente malucos, mas fundamentalmente engraçados e legais, são muito divertidos.
Eles lembram a internet antiga. A época em que as pessoas criavam coisas só para serem bobas e se divertir. Ontem à noite, inspirado pelo que ele postou, fiz um quase quine bem engraçado que faz a página imprimir seu próprio código-fonte, usando descompilação BEAM e alguns truques.
Eu queria ter tempo para produzir coisas assim sem parar, e só o fato de existirem pessoas que continuam criando coisas interessantes desse jeito já me faz sorrir.
Para quem quiser rir, a parte do sort-of quine está aqui: https://github.com/notactuallytreyanastasio/blog/blob/main/l...
Qualquer coisa que o Nolen faça é boa. Para mim, parece que ele acerta exatamente o sweet spot dos apps/sites de propósito único que me dão nostalgia de como era a internet antiga
Há também um experimento parecido do Matthew Rayfield que usava a barra de endereços em vez dos favicons das abas: https://www.youtube.com/watch?v=q7GtCLwTmV4
Eu com certeza tinha o trabalho dele com URLs em mente ao fazer isto, embora já fizesse bastante tempo desde a última vez que li o blog ou vi o vídeo. Foi muito divertido rever no meio do projeto e lembrar que ele também tinha ido na direção de “empilhar janelas para criar 2D”
Isso me lembra:
“Show HN: I saw this mind-blowing experiment, so I made a simple version of it” (25/11/2023) https://news.ycombinator.com/item?id=38413660
“Synchronize a 3D scene across multiple windows using Three.js and localStorage” (27/11/2023) https://news.ycombinator.com/item?id=38437773
Com certeza o próximo é Doom
Além disso, a cada quadro é provável que uma parte maior da tela mude, então para atualizar os favicons seria necessário fazer muito mais “converter canvas em URL de dados”. Na implementação atual, isso é caro. Eu não conhecia OffscreenCanvas, porém; talvez isso ajude
Se o problema for as abas ficarem mais estreitas no Firefox, ainda dá para inspecionar o próprio navegador Firefox, encontrar a regra de estilo da largura mínima das abas e colocá-la no userChrome.css.
Existem alguns guias sobre isso, e provavelmente alguém já organizou um para largura de abas.
Mas talvez isso seja trapaça. Também existe
browser.tabs.tabMinWidth, mas, se bem me lembro, ele ainda impõe um valor mínimoA banda Ok Go fez há algum tempo um videoclipe em colaboração com o Google Chrome, em que as janelas do navegador e os dançarinos ficavam sincronizados de um jeito muito legal, com efeitos tipo caleidoscópio também. Isso me fez lembrar dele
https://www.youtube.com/watch?v=ISL1GfXwr-o