PixelFairy — Companheira Pixel Art para o VSCode
Fada em pixel art que fica grudada no cursor de texto, dentro do próprio
editor de código, reagindo ao que você faz (parecido com extensões como o
Cursor Duck / Rainbow Fart). Os sprites são SVG desenhados via código (sem
nenhum arquivo de imagem de terceiros).
Como rodar localmente
- Instale as dependências:
npm install
- Compile:
npm run compile
- Abra a pasta no VSCode e pressione F5 — isso abre uma nova janela
("Extension Development Host") já com a extensão ativa.
- Abra qualquer arquivo de código nessa janela de teste. A fada aparece
colada logo depois do seu cursor de texto, dentro do próprio editor —
não numa aba separada. Pra ligar/desligar manualmente:
Ctrl+Shift+P → "PixelFairy: Mostrar/Ocultar Fada".
Os sprites SVG (media/sprites/*.svg) já vêm prontos no repositório — não
precisa (e não deve) rodar scripts/generate-sprites.js pra usar a
extensão. Veja "Como personalizar a fada" abaixo antes de mexer neles.
Estrutura
pixel-fairy/
├── package.json # manifesto da extensão
├── tsconfig.json
├── scripts/
│ ├── generate-sprites.js # legado: gera pixel art simples via grade (não usado pelos sprites atuais)
│ └── generate-icon.js # gera media/icon.png (ícone do Marketplace)
├── src/
│ └── extension.ts # escuta eventos do editor e desenha
│ # a decoração (ícone) na posição do cursor
└── media/
├── icon.png # ícone da extensão no Marketplace (128x128)
└── sprites/
├── idle.svg
├── typing.svg
├── deleting.svg
├── nav.svg
└── sleep.svg
Como funciona tecnicamente
Em vez de um Webview (painel separado), a extensão usa a API
vscode.window.createTextEditorDecorationType, que permite desenhar
conteúdo (nesse caso, um ícone SVG) grudado numa posição exata do texto —
é o mesmo mecanismo que o VSCode usa pra mostrar, por exemplo, o texto
acinzentado de sugestões do GitHub Copilot depois do cursor.
A cada evento (digitar, apagar, navegar), a extensão:
- Descarta a decoração anterior (
dispose())
- Cria uma nova decoração apontando pro SVG do estado certo
- Aplica essa decoração numa
Range de largura zero, exatamente na
posição atual do cursor (editor.selection.active)
Por isso os sprites precisam ser arquivos .svg reais em disco (não
strings de SVG no JS como na primeira versão) — a API de decoração exige
um caminho de arquivo (contentIconPath), não HTML/SVG inline.
Estados já implementados
| Estado |
Gatilho |
idle |
estado inicial / padrão |
typing |
você digitando/inserindo texto |
deleting |
você apagando texto (rangeLength > 0) |
nav |
movendo o cursor sem digitar |
sleep |
4s sem nenhum evento (ajuste IDLE_MS) |
Como personalizar a fada
Os sprites atuais (media/sprites/idle.svg, typing.svg, deleting.svg,
nav.svg, sleep.svg) são ilustrações vetoriais completas (paths
desenhados, não uma grade de retângulos), feitas num editor externo
(Aseprite/Illustrator/Figma etc.) e exportadas como SVG. extension.ts não
sabe nem se importa como o SVG foi feito — ele só lê o arquivo pelo nome do
estado (media/sprites/<estado>.svg).
Pra trocar o design: exporte o novo frame como .svg com o mesmo nome do
estado e substitua o arquivo em media/sprites/. Dois cuidados importantes
depois de trocar:
- Otimize com SVGO antes de commitar. Ilustrações vetoriais
exportadas direto de programas de desenho costumam vir com centenas de
milhares de pontos e alguns MB por arquivo — pesado pro pacote da
extensão e lento pra redesenhar a cada tecla digitada (a fada é
recriada a cada evento, veja "Como funciona tecnicamente"). Rode:
npx svgo media/sprites/<estado>.svg --multipass --precision=0
Isso já reduziu os sprites atuais de ~21MB pra ~1,8MB no total sem
perda visível (o precision baixo não importa numa imagem exibida a
~15-20px).
- Não deixe
width/height fixos na tag <svg> — só viewBox. Um
width/height explícito no arquivo atropela o tamanho calculado por
getIconSize() e a fada volta a aparecer gigante.
scripts/generate-sprites.js é um gerador legado, de uma versão
anterior mais simples (pixel art via grade de caracteres + PALETTE) —
não é usado pelos sprites atuais e rodá-lo de novo sobrescreve
media/sprites/*.svg pelo estilo antigo. Só use se quiser voltar
propositalmente pro visual de pixel art em blocos:
h = cabelo (PALETTE.hair)
s = pele (PALETTE.skin)
d = vestido (PALETTE.dress)
w / e = asa e borda da asa
o / X = contorno/detalhes escuros
. = vazio (transparente)
Esse mesmo esquema de grade + PALETTE ainda é usado de verdade em
scripts/generate-icon.js, que gera media/icon.png (o ícone 128x128 do
Marketplace) — pra mudar as cores ou a pose do ícone, edite a grade GRID
nesse arquivo e rode node scripts/generate-icon.js de novo.
O tamanho da fada é calculado dinamicamente em getIconSize()
(extension.ts), a partir do editor.fontSize atual (fontSize - 1), e
não por um valor fixo em pixels. Isso é proposital: uma decoração de texto
não pode ficar maior que a altura da linha do editor sem quebrar — ou o
VSCode estica a linha (some o alinhamento do clique do mouse com o cursor),
ou o ícone vaza e sobrepõe o texto vizinho. Como fontSize é sempre menor
ou igual à altura da linha, subtrair 1px garante que a fada sempre caiba.
Limitação conhecida: se o usuário tiver um editor.lineHeight
customizado menor que o fontSize (incomum, mas configurável), a margem de
1px pode não ser suficiente. Não há como resolver isso de forma genérica
sem ler a altura de linha renderizada de verdade, que a API de extensões do
VSCode não expõe.
Publicar no Marketplace (opcional)
npm install -g @vscode/vsce
vsce package # gera o arquivo .vsix
vsce publish # precisa de Personal Access Token do Azure DevOps
Antes de publicar, troque "publisher" no package.json pelo seu nome de
publisher registrado no Marketplace.
Próximos passos sugeridos
- Adicionar sons (como o QuackTrack e o Quacky Click fazem) usando a API
de webview +
<audio> com arquivos de som livres de direitos (CC0).
- Animar entre frames dentro de cada estado (ex.: 2-3 variações de
typing
alternando via setInterval no próprio fairy.js) pra dar sensação de
movimento contínuo, não só troca de pose estática.
- Já resolvido nesta versão: a fada segue o cursor de texto de verdade,
usando
TextEditorDecorationType grudado na posição do cursor.