1 pontos por GN⁺ 2025-02-22 | 1 comentários | Compartilhar no WhatsApp
  • 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 windowIndex e tabIndex da query da URL
  • O setInterval em 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 canvas HTML 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 windowIndex e tabIndex
    • A última aba aberta recebe parâmetros adicionais como isMain=true, numWindows, numTabs e fullWidth

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 head do 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 para data URL e envia isso ao documento principal
    • O documento principal troca o href de link rel="shortcut icon" para atualizar de fato o favicon

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 windowIndex e tabIndex dos 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 registration pelo broadcast channel com seus índices de aba/janela, e a aba principal responde com ack
  • 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 canvas real e os favicons acima foram tratados como um único canvas expandido
  • 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 canvas a 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 canvas real
  • Se um retângulo estiver abaixo da barra de URL, ele é desenhado no canvas real; 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 canvas travava, e nem desenhar dentro de requestAnimationFrame deixava 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 canvas resultante 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 canvas em 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 canvas e a barra de abas
    • Esse movimento combinava bem com o efeito visual do projeto

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

 
GN⁺ 2025-02-22
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

    • Falando entre pessoas que criaram Pongs absurdos, é um trabalho muito legal. Esse tipo de arte performática técnica é divertido tanto de fazer quanto de ver. Talvez tenha a ver com o motivo pelo qual faço malabarismo: “com certeza deve haver um jeito de tornar isso mais difícil”
    • Você resolveu essa parte? Como material de design para um jogo simples em JavaScript, o blog do Jake Gordon é bom. Ele usa um loop de jogo com double buffering com canvas frontal/traseiro.
      [0] https://jakesgordon.com/writing/javascript-pong/part1/
    • Muito bom, e seu ritmo de trabalho é realmente impressionante!
      Depois do 1 Million Checkboxes, você fez isto, global cap locks, regex de bad apple, uuid etc.; o embalo é impressionante
    • Agora fiquei com vontade de ver Doom rodando em abas do navegador
  • 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

    • O trabalho do Matt é uma grande inspiração para mim!
      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

    • Doom dá um pouco de trabalho. Pelo que sei, há um limite de 4 quadros por segundo, que é a frequência com que dá para atualizar as abas.
      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 esse é o seu gosto, acho que o Nolen já rodou Doom no iPhoto
  • 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ínimo

  • A 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