Pular para o conteúdo
miblo

Animações na tela do Miblo

O SDK fatia, o Miblo anima: a arte vai uma vez para a flash e o próprio aparelho calcula cada quadro. Conceitos, engenharia e receitas para tirar o melhor da tela.

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:

  1. Um efeito pronto num item do card (fx): uma palavra, nenhuma arte. Comece por aqui.

  2. screen.animate('arte.gif', { fps }): o SDK faz todo o resto.

  3. 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 redesenhadaPixelsTempo a 40 MHzTempo a 80 MHz
Um tile de 8x864~0,04 ms~0,02 ms
Um sprite de 16x16256~0,1 ms~0,05 ms
Um sprite de 32x321.024~0,4 ms~0,2 ms
100 tiles (uma animação típica)6.400~4 ms~2 ms
Uma região de 120x12014.400~7 a 10 ms~4 a 6 ms
A tela inteira, 240x24057.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:

  1. Decodifica a arte (o decodificador de GIF do bridge não tem dependências).

  2. Quantiza para 64 cores e escolhe uma subpaleta de 16 para cada tile.

  3. 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.

  4. Monta os mapas de cada quadro e as diferenças entre eles.

  5. Acha os sprites: objetos pequenos sobre fundo transparente que mudam de lugar viram
    sprites, não tiles novos.

  6. Escolhe o caminho, sem você pedir:

A arteO caminhoPor quê
Muitos quadros numa região pequenaUma folha de sprites num slotMais quadros por slot, o fps mais alto
Pixel art, interface, fundo repetidoTiles e mapasSó os tiles que mudam são redesenhados
Objetos se movendo sobre um fundo paradoSprites sobre o mapaO fundo nunca é redesenhado
Mudanças grandes numa parte da telaQuadros retangularesPaga só a área que muda
Mais de 256 tiles diferentes (uma foto)Quadros inteiros, até 4 fpsNão há como fatiar uma foto
fx num item, ou fx com speedEfeito do firmwareNenhuma arte por quadro
  1. 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.

  2. 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.

fxEm que itemO que fazCusto por quadro
sweepring, barO anel ou a barra enche até o valor novo~1 ms
countbigO número conta até o valor novo~1 ms
slidesparkO gráfico desliza quando chega um ponto novo~2 ms
pulsequalquerUm brilho curto quando o valor muda~1 ms
blinkqualquerPisca, para chamar a atenção~0,5 ms
orbitringUm ponto gira em volta do anel (a ISS)~0,5 ms
rainqualquerLinhas caindo sobre o item (o app Clima)~2 ms
tickertextO 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.

fxParâmetrosO que fazTela inteiraRegião de 120x120
scrollspeed px/s (-120 a 120; negativo para a esquerda)Rola a camada; várias camadas a velocidades diferentes fazem parallax~8 fps~15 fps
rotatespeed graus/s (-180 a 180), centreGira a camada em volta do centro~4 a 8 fps~15 fps
zoomspeed fator/s (0,25 a 4), centreAproxima ou afasta~4 a 8 fps~15 fps
perspectivespeed (0 a 10, a velocidade do chão)Inclina a camada até o horizonte: uma estrada~4 a 8 fps~15 fps
waveamplitude 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). speed em
    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 e
mesh com spin no logo. O código de cada cena está em apps/lib/demo-scenes.js, e o
miblo 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 flash8 (sempre sobram 350 KB ou mais para os pets e o sistema)
Tamanho de um quadro inteiroaté 60 KB; quadros de animação até ~12 KB comprimidos
Passos de uma animaçãoaté 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 retangularesaté 10 fps
Tiles e spritesaté 30 fps (estimado, medido no aparelho antes do lançamento)
Conjunto de tilesaté 256 tiles de 8x8 (8 KB), 16 cores por tile, 64 na tela
Mapa30x30 tiles (900 bytes)
Spritesaté 16, de 8x8 a 32x32, com uma cor transparente
Efeitos fx de card20 a 30 fps (estimado, medido no aparelho antes do lançamento)
Mode 7tela inteira ~4 a 8 fps, região de 120x120 ~15 fps (estimado, medido no aparelho antes do lançamento)
meshaté 64 vértices e 128 faces; flat numa região de até 120x120
Gravações na flash1 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 dizO que aconteceuO que fazer
"20 quadros: o Miblo guarda 16; ficaram os 16 mais diferentes"A arte tinha passos demaisMenos quadros, ou aceite a redução
"180 cores: ficaram 64, veja a prévia"A arte foi quantizadaDesenhe com menos cores
"detalhada demais para tiles: quadros inteiros a 4 fps"Mais de 256 tiles diferentesMais á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çamentoDiminua a região ou o que muda
"fps 60 acima do limite: 30"Um parâmetro foi ajustado ao limitePeça um valor dentro da tabela
"os 8 slots estão em uso"Não cabe mais arteReaproveite imagens ou tire as que não usa
"limite de gravações do slot por hoje"Uploads demais no diaNã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 8

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

  • drawMs cabe 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.

  • drawMs perto 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.

Voltar para a documentação