Este guia explica como o Miblo anima a tela e como tirar o melhor dele no seu app. A regra que
vale para tudo: o SDK fatia, o Miblo anima. O seu programa manda a arte uma vez e alguns números;
o próprio aparelho calcula cada quadro. Nada é transmitido enquanto a animação roda.
São três níveis, e só três:
Um efeito pronto num item do card (
fx): uma palavra, nenhuma arte. Comece por aqui.screen.animate('arte.gif', { fps }): o SDK faz todo o resto.A mesma chamada com opções (
fps,loop,region,fx,speed,amplitude,centre): Mode 7, parallax e 3D.
Slots, formatos, passos e tiles nunca aparecem no seu programa. Para parar tudo o que o seu app
anima: screen.stop().
Números marcados com (estimado, medido no aparelho antes do lançamento) vêm do projeto e das
contas; serão trocados pelos valores medidos num Miblo de verdade antes da versão sair.
1. Como o Miblo desenha
Sem frame buffer
A tela tem 240x240 pixels de 16 bits: uma imagem inteira ocupa 115 KB. O ESP8266 do Miblo tem
cerca de 50 KB de RAM livre. Não existe uma cópia da tela na memória: o Miblo desenha direto no
painel, em faixas, e o que já está no vidro fica lá até ser coberto. Tudo o que segue vem disso.
O custo é o barramento
Cada pixel viaja pelo barramento SPI até o painel. O custo de desenhar é, quase todo, quantos
pixels passam por ele:
| Área redesenhada | Pixels | Tempo a 40 MHz | Tempo a 80 MHz |
|---|---|---|---|
| Um tile de 8x8 | 64 | ~0,04 ms | ~0,02 ms |
| Um sprite de 16x16 | 256 | ~0,1 ms | ~0,05 ms |
| Um sprite de 32x32 | 1.024 | ~0,4 ms | ~0,2 ms |
| 100 tiles (uma animação típica) | 6.400 | ~4 ms | ~2 ms |
| Uma região de 120x120 | 14.400 | ~7 a 10 ms | ~4 a 6 ms |
| A tela inteira, 240x240 | 57.600 | ~25 a 40 ms | ~13 a 20 ms |
(estimado, medido no aparelho antes do lançamento)
A conclusão: animar a tela inteira é caro; animar o que muda é barato. Um efeito que mexe em
poucos tiles roda a 30 quadros por segundo; o mesmo efeito na tela inteira, a 4 a 8.
Um laço só, dividido com o Wi-Fi e o pet
O Miblo tem um único laço principal. Nele rodam o Wi-Fi, o pet, os alertas das suas sessões, o
relógio e a sua animação. Por isso o desenho é feito em faixas e devolve o controle entre uma
faixa e outra: o Wi-Fi nunca espera uma tela inteira.
O governador de quadros
Cada animação tem um orçamento de tempo por quadro. Se um quadro passa do orçamento, o
governador baixa a taxa daquela animação (por exemplo, de 30 para 15 fps) até caber de novo.
Ele nunca deixa a animação atrasar o Wi-Fi, o pet ou um alerta. Se a sua animação parece mais
lenta do que o fps pedido, foi o governador: veja Depurando com drawMs (#depurando-com-drawms).
2. O modelo do NES
O Nintendinho não tinha frame buffer e tinha 2 KB de RAM. Ele desenhava com três peças, e o Miblo
faz igual.
Tiles
Um tile é um desenho de 8x8 pixels guardado uma vez. Cada pixel é um índice de 4 bits numa
paleta de 16 cores (tirada das 64 do card): 32 bytes por tile. Um conjunto de tiles tem até
256 tiles (até 8 KB), fica num slot da flash e é copiado para a RAM enquanto a animação toca.
Mapas
A tela é uma grade de 30x30 tiles (240 / 8). Um mapa diz qual tile vai em cada casa: 900
bytes para a tela toda, contra 115 KB de uma imagem. Uma animação é uma sequência de mapas. Entre
um mapa e o seguinte, o Miblo redesenha só as casas cujo índice mudou.
Sprites
Um sprite é um objeto que se move sobre o mapa: até 16 deles, de 8x8 ou 16x16 (até 32x32),
com uma cor transparente. A cada passo, o Miblo restaura os tiles sob a posição antiga e desenha
o sprite na nova: cerca de 0,1 ms por sprite (estimado, medido no aparelho antes do lançamento).
O fundo nunca é redesenhado inteiro.
Paletas
O card usa 64 cores. Cada tile escolhe uma subpaleta de 16 delas: 4 bits por pixel, mas a
tela inteira pode ter as 64 cores.
O que veio do Super Nintendo
Espelhamento: um tile pode ser desenhado espelhado na horizontal ou na vertical. Um desenho
simétrico usa metade dos tiles.Duas prioridades: atrás ou na frente dos itens do card.
Sprites de até 32x32, com mistura contra o tile de baixo (a sombra translúcida da ISS no
Demo).Rolagem do mapa por tiles inteiros: um fundo que se repete rola quase de graça. Rolar por
pixel redesenha tudo, então fica em até 8 fps.
Por que isso dá de 8 a 30 fps aqui
Uma animação típica muda de 50 a 150 tiles por quadro. A ~40 µs por tile, isso é 2 a 6 ms por
quadro: sobra tempo para 30 fps. Mesmo redesenhando todos os 900 tiles, são 25 a 40 ms, ou seja,
pelo menos 8 fps (estimado, medido no aparelho antes do lançamento). E nada sai da flash no
caminho crítico: tiles e mapas já estão na RAM.
3. O que o SDK faz com a arte
Você manda um GIF animado, um APNG, uma lista de PNGs ou uma folha de sprites ({ sheet, cell,
count }). O SDK, no seu computador, faz o trabalho que um artista do NES fazia à mão:
Decodifica a arte (o decodificador de GIF do bridge não tem dependências).
Quantiza para 64 cores e escolhe uma subpaleta de 16 para cada tile.
Corta em tiles de 8x8 e remove os repetidos em todos os quadros, inclusive os que são
o espelho de outro. Pixel art e interface se repetem muito.Monta os mapas de cada quadro e as diferenças entre eles.
Acha os sprites: objetos pequenos sobre fundo transparente que mudam de lugar viram
sprites, não tiles novos.Escolhe o caminho, sem você pedir:
| A arte | O caminho | Por quê |
|---|---|---|
| Muitos quadros numa região pequena | Uma folha de sprites num slot | Mais quadros por slot, o fps mais alto |
| Pixel art, interface, fundo repetido | Tiles e mapas | Só os tiles que mudam são redesenhados |
| Objetos se movendo sobre um fundo parado | Sprites sobre o mapa | O fundo nunca é redesenhado |
| Mudanças grandes numa parte da tela | Quadros retangulares | Paga só a área que muda |
| Mais de 256 tiles diferentes (uma foto) | Quadros inteiros, até 4 fps | Não há como fatiar uma foto |
fx num item, ou fx com speed | Efeito do firmware | Nenhuma arte por quadro |
Converte, escolhe os slots e envia cada parte. Da próxima vez, só reenvia o que mudou (pelo
hash): se a arte é a mesma, nada é gravado de novo na flash.Confere os limites (8 slots, os fps, as gravações do dia) antes de qualquer coisa chegar
ao Miblo, e explica qualquer recusa em palavras simples (veja
Lendo as mensagens do SDK (#lendo-as-mensagens-do-sdk)).
4. Desenhando arte que anima bem
A arte certa é a diferença entre 30 fps e 4 fps. As regras:
Desenhe na grade de 8 px. Bordas, faixas e objetos alinhados a múltiplos de 8 viram tiles
inteiros e repetidos. Uma linha que cruza um tile no meio cria tiles novos.Áreas chapadas. Uma cor lisa é um tile só, repetido quantas vezes for preciso.
Repita. Grama, água, estrelas, asfalto: um padrão de 8, 16 ou 32 px que se repete custa
poucos tiles e rola de graça.Poucos degradês. Um degradê suave (ou pontilhado) gera um tile diferente em cada casa. Use
faixas de 2 ou 3 tons.Fundo parado, poucos objetos em movimento. Um cenário fixo com um carro, um peixe e um pet
andando é o caso perfeito: o fundo vai uma vez, os objetos viram sprites.Até 16 cores por tile. A tela pode ter 64, mas cada tile de 8x8 usa no máximo 16.
Fotos não são animações. Uma foto tem um tile diferente em cada casa: acaba em quadros
inteiros a 4 fps. Arte é animação; foto é fundo (bg: 'frame:N').
O Demo segue tudo isso: a estrada (128x128) tem 224 bytes, o céu estrelado 349 bytes, a água 285
bytes. As oito imagens somam 12 KB e são desenhadas por código em apps/scripts/demo-assets.mjs.
5. Efeitos prontos e Mode 7
Efeitos num item do card (fx)
Um item do card pode ter fx. O firmware calcula o efeito em regiões pequenas, a 20 a 30 fps: sem
quadro, sem upload, sem desgaste da flash. É o jeito mais seguro e mais suave de animar.
fx | Em que item | O que faz | Custo por quadro |
|---|---|---|---|
sweep | ring, bar | O anel ou a barra enche até o valor novo | ~1 ms |
count | big | O número conta até o valor novo | ~1 ms |
slide | spark | O gráfico desliza quando chega um ponto novo | ~2 ms |
pulse | qualquer | Um brilho curto quando o valor muda | ~1 ms |
blink | qualquer | Pisca, para chamar a atenção | ~0,5 ms |
orbit | ring | Um ponto gira em volta do anel (a ISS) | ~0,5 ms |
rain | qualquer | Linhas caindo sobre o item (o app Clima) | ~2 ms |
ticker | text | O texto rola uma vez, quando não cabe | ~1 ms |
(estimado, medido no aparelho antes do lançamento)
Os efeitos rodam a cada atualização do card: mande o valor novo, o Miblo anima a mudança. Nada
muda na API.
Efeitos de camada: Mode 7
Com fx em screen.animate, a arte vira uma camada de tiles na RAM (até 8 KB de tiles e um mapa
de 900 bytes) e o Miblo calcula cada linha com matemática de inteiros. Só a arte (uma vez) e três
números saem do computador: é a animação mais segura que existe.
fx | Parâmetros | O que faz | Tela inteira | Região de 120x120 |
|---|---|---|---|---|
scroll | speed px/s (-120 a 120; negativo para a esquerda) | Rola a camada; várias camadas a velocidades diferentes fazem parallax | ~8 fps | ~15 fps |
rotate | speed graus/s (-180 a 180), centre | Gira a camada em volta do centro | ~4 a 8 fps | ~15 fps |
zoom | speed fator/s (0,25 a 4), centre | Aproxima ou afasta | ~4 a 8 fps | ~15 fps |
perspective | speed (0 a 10, a velocidade do chão) | Inclina a camada até o horizonte: uma estrada | ~4 a 8 fps | ~15 fps |
wave | amplitude px (0 a 16), speed ondas/s (0 a 4) | Desloca cada linha na horizontal: água, calor | ~8 fps | ~15 fps |
(estimado, medido no aparelho antes do lançamento) Os intervalos são os que o SDK aceita; um
valor fora deles é ajustado ao limite.
Toda camada tem uma region ({ x, y, w, h }, dentro da tela). Quanto menor a região, maior o
fps. O governador vale aqui também.
3D: mesh
screen.animate({ mesh }, { fx, speed, shading, region }) desenha um modelo 3D pequeno, girado
pelo firmware com matemática de inteiros.
mesh:'cube','icosphere'ou{ vertices, faces }(até 64 vértices e 128 faces; vértices[x, y, z], faces com índices de vértices).fx:spin(gira em torno de um eixo),orbit,tumble(rola em dois eixos).speedem
graus/s (-360 a 360).shading: 'wire'(linhas; só os tiles sob as linhas antigas e novas são redesenhados: 20 a 30
fps) ou'flat'(triângulos cheios com uma luz simples, numa região de até 120x120: 10 a 15 fps)
(estimado, medido no aparelho antes do lançamento).
Exemplos em movimento
Cada efeito acima aparece no app Demo (loja, categoria Delícias): perspective e scroll na
estrada, rotate no planeta, wave na água, rain sobre a água, os efeitos de card no painel emesh com spin no logo. O código de cada cena está em apps/lib/demo-scenes.js, e omiblo screen preview mostra cada efeito no seu computador antes de mandar.
6. Receitas
Cada receita tem no máximo 15 linhas e roda como está (Node 20+, @miblo/status). As imagens são
as do Demo (apps/assets/demo).
Chuva
// recipe: rain
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'Clima' });
// A chuva é um efeito do firmware sobre o item: nenhuma arte, nenhum upload.
await screen.card({
v: 1, title: 'São Paulo', icon: 'cloud',
items: [
{ t: 'big', value: '21°', label: 'chuva forte', color: 'blue' },
{ t: 'text', value: 'chuva até as 16h', fx: 'rain' },
],
});Órbita
// recipe: orbit
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'ISS' });
// Um ponto gira em volta do anel: a estação dando a volta na Terra.
await screen.card({
v: 1, title: 'ISS', icon: 'star',
items: [
{ t: 'ring', value: 0.62, label: 'volta atual', color: 'blue', fx: 'orbit' },
{ t: 'text', value: 'sobre o Oceano Atlântico' },
],
});Pulso
// recipe: pulse
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'Vendas' });
// Brilha uma vez a cada valor novo: mande o card de novo quando o número mudar.
await screen.card({
v: 1, title: 'Vendas', icon: 'chart',
items: [{ t: 'big', value: '128', label: 'hoje', color: 'green', fx: 'pulse' }],
});Estrada
// recipe: road
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'Estrada' });
// Três camadas: o céu longe e devagar, o mar no meio, a estrada vindo na sua direção.
await screen.animate('stars.png', { fx: 'scroll', speed: -4, region: { x: 0, y: 0, w: 240, h: 72 } });
await screen.animate('water.png', { fx: 'scroll', speed: -20, region: { x: 0, y: 72, w: 240, h: 40 } });
await screen.animate('road.png', { fx: 'perspective', speed: 6, region: { x: 0, y: 112, w: 240, h: 128 } });
// O carro é um GIF com fundo transparente: o SDK faz dele um sprite.
await screen.animate('car.gif', { fps: 8, region: { x: 40, y: 192, w: 160, h: 40 } });Água
// recipe: water
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'Aquário' });
// A onda desloca cada linha da água; o peixe nada por cima.
await screen.animate('water.png', { fx: 'wave', amplitude: 4, speed: 2, region: { x: 0, y: 120, w: 240, h: 120 } });
await screen.animate('fish.gif', { fps: 9, region: { x: 0, y: 152, w: 240, h: 48 } });Contador
// recipe: counter
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'Downloads' });
// Mande só o valor novo: o Miblo conta do anterior até ele.
const total = 48213;
await screen.card({
v: 1, title: 'Downloads', icon: 'chart',
items: [{ t: 'big', value: total.toLocaleString('pt-BR'), label: 'esta semana', fx: 'count' }],
});Letreiro
// recipe: ticker
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'Manchetes' });
// Um texto que não cabe rola uma vez, do começo ao fim.
await screen.card({
v: 1, title: 'Manchetes', icon: 'mail',
items: [{ t: 'text', value: 'Copom mantém a Selic e sinaliza cortes', fx: 'ticker' }],
});Cubo
// recipe: cube
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'Cubo' });
// Um cubo de arame girando: o Miblo calcula cada quadro, só as linhas são redesenhadas.
await screen.animate({ mesh: 'cube' }, { fx: 'spin', speed: 90, shading: 'wire', region: { x: 60, y: 60, w: 120, h: 120 } });Um GIF qualquer
// recipe: sprite
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'Pet' });
// O nível 2: um GIF e o fps. Com loop: false ele toca uma vez.
await screen.animate('pet.gif', { fps: 8, loop: false, region: { x: 0, y: 204, w: 240, h: 32 } });
// Mais tarde, para tirar tudo o que o seu app anima:
await screen.stop();7. Limites, mensagens e depuração
Limites
| O quê | Limite |
|---|---|
| Slots de animação na flash | 8 (sempre sobram 350 KB ou mais para os pets e o sistema) |
| Tamanho de um quadro inteiro | até 60 KB; quadros de animação até ~12 KB comprimidos |
| Passos de uma animação | até 16, de 50 a 2000 ms cada |
| Quadros inteiros (tela toda) | até 4 fps; meta de 8 fps ou mais (estimado, medido no aparelho antes do lançamento) |
| Quadros retangulares | até 10 fps |
| Tiles e sprites | até 30 fps (estimado, medido no aparelho antes do lançamento) |
| Conjunto de tiles | até 256 tiles de 8x8 (8 KB), 16 cores por tile, 64 na tela |
| Mapa | 30x30 tiles (900 bytes) |
| Sprites | até 16, de 8x8 a 32x32, com uma cor transparente |
Efeitos fx de card | 20 a 30 fps (estimado, medido no aparelho antes do lançamento) |
| Mode 7 | tela inteira ~4 a 8 fps, região de 120x120 ~15 fps (estimado, medido no aparelho antes do lançamento) |
mesh | até 64 vértices e 128 faces; flat numa região de até 120x120 |
| Gravações na flash | 1 por slot a cada 10 s, 100 por slot por dia |
As gravações valem para o upload da arte; tocar uma animação só lê a flash. Use as mesmas imagens
entre cenas (como o Demo faz com as estrelas, a água e a estrada) e o SDK não grava nada de novo.
Lendo as mensagens do SDK
O SDK confere tudo antes de mandar e diz o que fez em palavras simples. As mensagens têm este teor:
| A mensagem diz | O que aconteceu | O que fazer |
|---|---|---|
| "20 quadros: o Miblo guarda 16; ficaram os 16 mais diferentes" | A arte tinha passos demais | Menos quadros, ou aceite a redução |
| "180 cores: ficaram 64, veja a prévia" | A arte foi quantizada | Desenhe com menos cores |
| "detalhada demais para tiles: quadros inteiros a 4 fps" | Mais de 256 tiles diferentes | Mais áreas chapadas e repetição, ou uma região menor |
| "pesada demais para 8 fps: 4 fps, ou uma região menor" | O quadro passa do orçamento | Diminua a região ou o que muda |
| "fps 60 acima do limite: 30" | Um parâmetro foi ajustado ao limite | Peça um valor dentro da tabela |
| "os 8 slots estão em uso" | Não cabe mais arte | Reaproveite imagens ou tire as que não usa |
| "limite de gravações do slot por hoje" | Uploads demais no dia | Não reenvie a mesma arte; ela já está lá |
miblo screen preview
Antes de mandar qualquer coisa, veja exatamente o que o Miblo vai mostrar, pixel a pixel:
miblo screen preview car.gif --fps 8A prévia roda o mesmo núcleo (WebAssembly) que o app do computador e o celular usam: as mesmas
cores, os mesmos tiles, os mesmos efeitos. Ela mostra as mensagens do SDK e o caminho escolhido
(tiles, sprites, quadros ou efeito). A mesma prévia está na aba Apps do app Miblo.
Depurando com drawMs
O Miblo mede quanto tempo leva para desenhar e conta no status da tela
(GET /sdk/v1/screen, ou await screen.status()): drawMs, o tempo do último desenho, e o fps
real da animação. O Demo mostra esses dois números no canto.
const st = await screen.status();
console.log(st.anim?.fps ?? st.fps, 'fps', st.anim?.drawMs ?? st.drawMs, 'ms');Como ler:
drawMscabe no orçamento (abaixo de 1000 / fps): tudo certo.O fps real é menor que o pedido: o governador agiu. Diminua a
region, troque quadros por
tiles (arte mais chapada e repetida) ou peça um fps menor.drawMsperto de 25 a 40 ms: a tela inteira está sendo redesenhada a cada quadro. Procure o
que muda sem precisar: um degradê, um ruído, uma foto.Picos de vez em quando: é o Wi-Fi ou o pet tendo a vez deles. É o esperado; o governador
cuida.