wiggle-that-diagram: movimento explicativo em três estilos
Transforme uma ilustração estática "como funciona" em uma animação curta, legendada e em loop que cabe dentro de um
slide HTML ou relatório, e ofereça-a em três formatos para que o leitor possa escolher. O método foi desenvolvido em um
deck de cliente real (consulte "Exemplo prático"). Ele usa huashu-art-motion para direção (listas de cenas,
regras de câmera, as seis maneiras de falhar) e, para os estilos A e B, como mecanismo de renderização.
Quando usar e quando não usar
Use quando:
- um deck ou relatório HTML tiver um diagrama de mecanismo (fluxo de arquivos, verificação de regras, uma pessoa aprova) e o leitor dever ver o que acontece, passo a passo, com palavras que possa ler;
- o usuário quiser opções em formatos genuinamente diferentes, não três esquemas de cores de um;
- uma animação anterior estava "muito rápida para ler" ou precisa de legendas.
Não use para: uma única microinteração (use animate), um vídeo narrado com voz over (use
huashu-art-motion diretamente) ou um gráfico de números (use dataviz).
Os três estilos (diferem no formato, não na paleta)
| A · clipe de diagrama | B · quadro branco | C · SVG ao vivo | |
|---|---|---|---|
| Imagem | Cena do canvas da engine huashu, gramática t1_3b1b, renderizada para um H.264 sem texto | Gramática huashu y3_whiteboard, quadro escuro, caneta desenha e letras à mão (Kalam), vídeo quadrado | Mundo SVG inline filmado por uma câmera virtual (viewBox animado por GSAP 3.12.5) |
| Palavras na imagem | Rótulos HTML colocados sobre o vídeo a partir de uma trilha de câmera descartada (sempre nítidos) | Desenhados no vídeo (devem ser grandes o suficiente para um telefone) | Texto SVG real (nítido, reflui alterando os quadros da câmera por largura) |
| Tamanho por filme (exemplo prático) | 360–820 KB (orçamento 900 KB) | 847–886 KB (orçamento 900 KB) | 21–34 KB (orçamento 40 KB) |
| Sensação | um diagrama limpo estilo 3Blue1Brown | alguém explicando em um quadro branco | uma animação de produto polida |
| Principal risco | rótulos se desviam se a cena muda sem re-renderizar | orçamento de tamanho apertado; rótulos de telefone 12–14 px | matemática de câmera complicada; peculiaridades de transformação do GSAP |
Todos os três compartilham UM script de legenda, as mesmas batidas e o mesmo contrato de incorporação, para que um seletor de estilo mostre a mesma história contada de três maneiras.
O método, do início ao fim
Cada etapa termina com a verificação que prova que está concluída.
-
Encontre o mecanismo e um exemplo concreto. Leia a página do host (somente leitura). Escolha o momento a mostrar e UM exemplo real que percorre todas as batidas (uma linha, um registro, um caso, com seus valores reais). Liste o que não deve ser inventado (ids, figuras, imagens). → Verifique: você pode dizer cada batida como "este objeto faz isto", com um substantivo e um verbo.
-
Escreva o script de legenda primeiro. Uma frase de legenda por batida, 3–6 batidas por filme, em
beats.json(formato:scripts/common/beats.example.json). As legendas são idênticas em todos os estilos. Em seguida, execute a verificação de ritmo e leia o piso que ela imprime:python3 ~/.claude/skills/wiggle-that-diagram/scripts/common/pace_check.py beats.json→ Verifique: toda batida OK, e o usuário concordou com os comprimentos de loop resultantes (ou legendas mais curtas). Nunca defina um alvo de loop separadamente das legendas; consulte "Ritmo".
-
Escreva o briefing. Preencha
references/brief-template.mdemSPEC.md: feedback citado, tokens, regra de ritmo, script de legenda, batidas, contrato de incorporação com o trecho literal, orçamentos, entregáveis. → Verifique: um agente sem nenhum outro contexto poderia construir a partir dele. -
Rodada de direções (opcional, quando o usuário não viu os estilos). Três agentes em paralelo, um por estilo, cada um prototipando o MESMO momento de um filme (~8–12 s) e capturando imagens estáticas em 1280 e 390 px. Mostre as imagens lado a lado; o usuário escolhe uma ou mantém as três. → Verifique: a escolha do usuário está por escrito.
-
Rodada de construção. Um agente por estilo (cada um continuando seu próprio protótipo), todos os filmes nesse estilo, escrevendo apenas em
<work>/<style>/final/. Briefings:references/agent-briefs.md. Diga a contagem de agentes e o nível ao usuário antes de lançar. → Verifique: cada um retorna arquivos, imagens estáticas, NOTES.md e uma lista honesta de problemas em aberto. -
Receitas de construção (o que cada agente de estilo realmente fez; os scripts estão em
scripts/style-a|b|c/): Dois filmes neutros são enviados em todos os três estilos, cada um comobeats.jsonmaisstyle-a/,style-b/,style-c/:examples/pizza/(5 batidas, simples) eexamples/expenses/(6 batidas: cartões, motivos codificados por cores, setas para um segundo e terceiro painéis). Comece pela pasta do seu estilo.- A.
cp -R ~/.claude/skills/huashu-art-motion/scripts/engine <A>/engineetouch <A>/engine/favicon.ico. Adicioneengine/ways/way_make.js(descripts/style-a/), executebeats_js.py beats.json --engine engine --var <VAR>(legendas e temporização do script compartilhado, nunca uma cópia manual), um arquivo de cenaengine/ways/<film>.jsque chamaWAY.make({poster, beats, camBase, cam, labels})e registraSCENES['<film>'], eengine/eras_<film>.jsdefinindoFILM_SIZE=[1280,720],PUNCH=0(caso contrário, a engine adiciona um zoom de 0.03 nas transições),SCENE_LIBS=['ways/way_make.js','ways/<film>_beats.js','ways/<film>.js'],SCENE_DIR='ways',ERAS=[{id:'<film>',dur:1}]. Não desenhe NENHUM texto no canvas. Os auxiliares de desenho (caixas arredondadas, pontos, linhas, fades, movimentos ao longo de um caminho) são escritos por filme;examples/pizza/style-a/pizza.jstem um pequeno conjunto para copiar. Em seguida,render_way.py <film> --scale 0.75 --crf 30→build.py <film> --meta meta.json→pace_check.py work/<film>_data.json→test_embed.py final/<film>.html:<verdict beat>(ou tudo de uma vez:make.sh). Após qualquer edição de cena, re-renderize e executemidmove.pynos tempos de meio movimento: os rótulos devem ficar sobre seus objetos. - B. Copie a engine da mesma forma (com o
favicon.icovazio: no Chrome real, seu 404 conta como um erro de página erender.pysai com 1 após escrever um bom vídeo) e corrijarender.pyparachromium.launch(channel='chrome', …). Coloquekit.jsemengine/demos/_hb/, escrevaengine/demos/<film>/board.js(usaHB.make(), ancora eventos aBEATS[k].t0 + offset) eengine/demos/<film>/eras.js(FILM_SIZE=[1080,1080],PUNCH=0, face da fonte Kalam,SCENE_LIBS=['demos/<film>/beats.js','demos/_hb/kit.js','demos/<film>/board.js']). Em seguida,build_beats.py beats.json --engine engine→uv run --with playwright python engine/render.py --film demos/<film> --fps 30 --crf 12 --out work/<film>_master.mp4→build_html.py <film> --beats beats.json(720², 24 fps, CRF mais baixo que caiba) →test_contract.py final/<film>.html --beats beats.json. Cada filme termina com um afastamento para todo o quadro, uma pausa de ≥ 1 s, depois uma limpeza com borracha; o quadro vazio é onde o loop recomeça. Ostillda última batida em beats.json é compartilhado por todos os estilos e deve cair antes do início da limpeza, ou o pôster e o quadro de movimento reduzido mostram um quadro meio apagado. Objetos que se movem (uma scooter, uma caixa) são itensK.customcomhanded: falsecuja posição é uma função do tempo; dê a cada um um tempo final, ou ele permanece no quadro após seu trabalho ser concluído. - C. Comece de
examples/pizza/style-c/(SVG, timeline, tema). Escrevasrc/<film>.svg(uma<svg id="scene" class="scene">contendo<g id="world">; marque cada<text>ou seu grupo comdata-b="<beat>", baseado em 1, definido para a batida em que a câmera MOSTRA pela primeira vez) esrc/<film>.js(quadros da câmeraFR/ORDER, cada quadro com um retângulowpara telas largas e um retângulotpara telas estreitas; revelações comR(selector, beat, offset), movimentos comcamTo(i, beat, offset), desenhos de linha comdraw()). No JS,beaté baseado em 0 (B[0]é a batida 1).R()é apenas para palavras, com offset ≥ 2.5 s: cada chamada deR()é registrada como uma revelação de palavra e a legenda conta como palavras no offset 0, portantoR()em uma forma sem palavras ou no offset 0 falha na verificação de espaçamento. Formas, movimento e palavras que chegam com a legenda usamtl.to(…, T(beat, offset))simples. Envolva qualquer elemento que tenha seu própriotransformem um<g>pai e anime o filho. Entãobuild.py beats.json --src src --out final [--css theme.css]→test_contract.py final/<film>.html --stills <scratch>/stills→widthcheck.py. O runtime (shell.html,shell.css,head.js,tail.js) é reutilizado como está; para corresponder à página do host, coloque novos tokens:root(--panel,--ink,--muted,--line,--gold,--cap-ink,--on-gold, …) em um arquivo de tema e passe-o com--css. Para uma imagem README ou chat de um filme:gif.py final/<film>.html --beat 4 --from 1.9 --to 8.6 --out film.gif(ou--still <s>para um PNG).
- A.
-
Verifique antes de mostrar a alguém (todas as três construções pularam as três últimas; faça-as):
- verificação de ritmo OK; todos os testes de contrato passam em 1100 e 360 px no Chrome real; tamanhos dentro do orçamento;
- olhe as folhas de contato de cada batida em ambas as larguras (
scripts/common/contact_sheet.py); - huashu
film_gate.pynos mp4s A/B (com o shim ffprobe, se necessário), vermelho lido conforme explicado emtraps.md; - um revisor independente que não construiu assiste a cada loop em velocidade normal e escreve as descobertas;
- teste dentro da página do host real (lado pai de
references/embed-contract.md), em um telefone real e no Safari (fallback do estilo A/B: pôster + legendas numeradas quando o H.264 não pode reproduzir).
-
Entrega. Mostre os três estilos lado a lado (imagens estáticas ou uma página de seletor), com os comprimentos do loop, tamanhos, o que foi inventado na imagem (para o usuário confirmar) e o que não foi verificado.
Lado do host: todos os nove em um slide com um seletor (scripts/deck/)
O que o deck de exemplo faz e o que quebrou no caminho:
- Incorporar, não vincular. Cada filme vai para o deck como
<script type="text/plain" id="anim-{style}-{way}">com</script→<\/scripte<!--→<\!--escapados; o JS o decodifica emiframe.srcdocapenas quando o slide é mostrado (um iframe, construído sob demanda).embed_anims.pyfaz isso de forma idempotente entre os marcadores<!--ANIM-DATA-START-->/<!--ANIM-DATA-END-->, então reexecutar após uma reconstrução substitui apenas os dados. O deck cresceu de ~0.3 MB para 4.5 MB. - Um slide, dois grupos de botões segmentados (Caminho, Estilo;
aria-pressed), mais um link "Veja como funciona" dentro de cada painel de detalhes do Caminho que define o caminho, fecha o painel e salta para o slide. - Play/pause segue o slide:
go()despacha um eventodeck:slide; o alternador postaanim-playquando seu slide se torna atual (e no carregamento do iframe, se atual),anim-pausecaso contrário. - Gestos sobre o iframe: os manipuladores de wheel/tecla/swipe do deck foram refatorados em
onWheel(dy,mode),onKey(key),onSwipe(dy,dx), chamados tanto pelos próprios listeners do deck quanto pelas mensagens encaminhadas do iframe (aceitar apenase.source === frame.contentWindow).howtest2.pyprova que um gesto = um slide. - Ajuste a uma tela de laptop: estreite o layout interno primeiro, zoom por último. Em 1280×800 os filmes tinham 740–840 px
de altura na largura total, então a legenda caía abaixo da tela. O que funciona (
howFitemembed_anims.py): o iframe srcdoc é de mesma origem, então o deck medecontentDocument.bodyaltura por conta própria. Ele tenta larguras internas da largura da caixa para baixo até 740 px (busca binária, esperando ~90 ms por largura porque um filme pode definir seu layout em JS no redimensionamento, quadro oculto enquanto isso) e mantém a mais larga que couber; somente se 740 px ainda for muito alto, ele defineframe.style.zoom. Estreitar vence o zoom porque a legenda mantém seu tamanho enquanto a imagem encolhe. Armadilhas encontradas no caminho: (a) zoom comwidth:100%alarga o layout interno (1126 → 1876 px) e faz um loop até o piso, então sempre fixeframe.style.widthem px; (b)documentElement.scrollHeightnunca relata menos do que a altura atual do iframe, então não pode conduzir o encolhimento (meçabodyem vez disso, com o quadro tornado alto); (c) medir no mesmo tick que a mudança de largura lê um layout JS obsoleto. Peça aos filmes um layout lado a lado quando largo (imagens quadradas) e ≤ 580 px de altura total a 780 px de largura. Telefones (< 900 px) não cabem; o slide rola, com um cabeçalho compacto. - Testes:
howtest.py(alternância, link profundo, setas),howtest2.py(todos os 9 carregam e reproduzem no Chrome real, wheel/tecla/swipe sobre o iframe movem exatamente um slide), além das próprias verificações de um gesto um slide do deck. Qualquer teste de deck que codifique números de slide de forma fixa quebra quando o slide é inserido: procure slides por id.
Ritmo (a principal reclamação do usuário sobre o primeiro corte)
Os primeiros protótipos tinham 8–13 s. O veredito do leitor: muito rápido para ler o que estava acontecendo, e precisava de legendas. A regra usada para a reconstrução:
- cada batida dura ≥ max(4,5 s, (palavras da legenda + novas palavras da imagem) / 3 + 1,5 s). A contagem canônica é
scripts/common/pace_check.py(e obuild.pydo estilo C, que conta da mesma forma a partir do SVG). O própriopace_check.pydo estilo A verifica os tempos reais dos rótulos do filme construído e difere em uma coisa: uma palavra repetida por um novo rótulo em uma batida posterior conta novamente lá; - algo novo aparece apenas ≥ 2,5 s após as palavras anteriores; a câmera fica parada enquanto as palavras são lidas;
- movimentos lentos da câmera entre as batidas (1,4–2,0 s, easing senoidal), no máximo UM acento rápido por filme, no veredito;
- ≥ 1 s de pausa no último quadro antes do loop.
Consequência a dizer ao usuário antecipadamente: legendas de ~20 palavras por batida fazem filmes de 5–6 batidas de 40–80 s. No
exemplo prático, os alvos de 18–34 s do briefing eram impossíveis (as legendas sozinhas precisavam de 31,5–51,7 s); todos os três
agentes escolheram a regra de leitura e construíram loops de 35–82 s, o que ninguém havia confirmado com o usuário. Loops mais curtos
significam legendas mais curtas, alteradas uma vez em beats.json para todos os estilos.
Contrato de incorporação, tamanho e telefones (resumo; texto completo em references/embed-contract.md)
- Um arquivo HTML autossuficiente por filme, carregado pelo host em
<iframe srcdoc>. Imagem + faixa de legenda + trilha de passos + Play/Pause, nada mais. Mídia embutida em base64;