wiggle-that-diagram: animación explicativa en tres estilos
Convierte una ilustración estática de "cómo funciona" en una animación corta, con subtítulos y en bucle, que se inserta dentro de una presentación HTML o informe, y ofrécela en tres formatos para que el lector pueda elegir. El método se desarrolló en una presentación real de un cliente (consulta "Ejemplo práctico"). Utiliza huashu-art-motion para la dirección (guiones de tomas, reglas de cámara, las seis formas de fallar) y, para los estilos A y B, como motor de renderizado.
Cuándo usarlo y cuándo no
Úsalo cuando:
- una presentación o informe HTML tiene un diagrama de mecanismo (flujo de archivos, verificación de reglas, aprobación por una persona) y el lector debería verlo ocurrir, paso a paso, con palabras que pueda leer;
- el usuario quiere opciones en formatos genuinamente diferentes, no tres esquemas de color de uno;
- una animación anterior era "demasiado rápida para leer" o necesita subtítulos.
No lo uses para: una sola microinteracción (usa animate), un video narrado con voz en off (usa huashu-art-motion directamente), o un gráfico de números (usa dataviz).
Los tres estilos (difieren en formato, no en paleta)
| A · diagrama clip | B · pizarra | C · SVG en vivo | |
|---|---|---|---|
| Imagen | Escena del lienzo del motor huashu, gramática t1_3b1b, renderizada a H.264 sin texto | Gramática huashu y3_whiteboard, pizarra oscura, bolígrafo dibuja y escribe a mano (Kalam), video cuadrado | Mundo SVG en línea filmado por una cámara virtual (viewBox animado con GSAP 3.12.5) |
| Palabras en la imagen | Etiquetas HTML colocadas sobre el video a partir de una pista de cámara volcada (siempre nítidas) | Dibujadas en el video (deben ser lo suficientemente grandes para un teléfono) | Texto SVG real (nítido, se reajusta cambiando los fotogramas de la cámara según el ancho) |
| Tamaño por película (ejemplo práctico) | 360–820 KB (presupuesto 900 KB) | 847–886 KB (presupuesto 900 KB) | 21–34 KB (presupuesto 40 KB) |
| Sensación | un diagrama limpio al estilo 3Blue1Brown | alguien explicando en una pizarra | una animación de producto pulida |
| Riesgo principal | las etiquetas se desvían si la escena cambia sin volver a renderizar | presupuesto de tamaño ajustado; etiquetas de teléfono 12–14 px | matemáticas de cámara complicadas; rarezas de transformación de GSAP |
Los tres comparten UN guión de subtítulos, los mismos tiempos y el mismo contrato de incrustación, por lo que un conmutador de estilos muestra la misma historia contada de tres maneras.
El método, de principio a fin
Cada paso termina con la verificación que demuestra que está completo.
-
Encuentra el mecanismo y un ejemplo concreto. Lee la página anfitriona (solo lectura). Elige el momento a mostrar y UN ejemplo real que recorra cada momento (una fila, un registro, un caso, con sus valores reales). Enumera lo que no se debe inventar (ids, cifras, imágenes). → Verificación: puedes decir cada momento como "este objeto hace esto", con un sustantivo y un verbo.
-
Escribe primero el guión de subtítulos. Una oración de subtítulo por momento, de 3 a 6 momentos por película, en
beats.json(formato:scripts/common/beats.example.json). Los subtítulos son idénticos en todos los estilos. Luego ejecuta la compuerta de ritmo y lee el piso que imprime:python3 ~/.claude/skills/wiggle-that-diagram/scripts/common/pace_check.py beats.json→ Verificación: cada momento está bien, y el usuario ha acordado las duraciones de bucle resultantes (o subtítulos más cortos). Nunca establezcas un objetivo de bucle por separado de los subtítulos; consulta "Ritmo".
-
Escribe el resumen. Completa
references/brief-template.mdenSPEC.md: comentarios citados, tokens, regla de ritmo, guión de subtítulos, momentos, contrato de incrustación con el fragmento textual, presupuestos, entregables. → Verificación: un agente sin otro contexto podría construirlo a partir de ahí. -
Ronda de direcciones (opcional, cuando el usuario no ha visto los estilos). Tres agentes en paralelo, uno por estilo, cada uno prototipando el MISMO momento de una película (~8–12 s) y tomando imágenes fijas a 1280 y 390 px. Muestra las imágenes fijas una al lado de la otra; el usuario elige una o conserva las tres. → Verificación: la elección del usuario está por escrito.
-
Ronda de construcción. Un agente por estilo (cada uno continúa su propio prototipo), todas las películas en ese estilo, escribiendo solo bajo
<work>/<style>/final/. Resúmenes:references/agent-briefs.md. Di al usuario la cantidad de agentes y el nivel antes de lanzarlos. → Verificación: cada uno devuelve archivos, imágenes fijas, NOTES.md y una lista honesta de problemas abiertos. -
Recetas de construcción (lo que realmente hizo cada agente de estilo; los scripts están en
scripts/style-a|b|c/): Dos películas neutrales se entregan en los tres estilos, cada una comobeats.jsonmásstyle-a/,style-b/,style-c/:examples/pizza/(5 momentos, simple) yexamples/expenses/(6 momentos: tarjetas, razones codificadas por colores, flechas a un segundo y tercer panel). Comienza desde la carpeta de tu estilo.- A.
cp -R ~/.claude/skills/huashu-art-motion/scripts/engine <A>/engineytouch <A>/engine/favicon.ico. Agregaengine/ways/way_make.js(descripts/style-a/), ejecutabeats_js.py beats.json --engine engine --var <VAR>(subtítulos y temporización del script compartido, nunca una copia manual), un archivo de escenaengine/ways/<film>.jsque llama aWAY.make({poster, beats, camBase, cam, labels})y registraSCENES['<film>'], yengine/eras_<film>.jsestableciendoFILM_SIZE=[1280,720],PUNCH=0(de lo contrario, el motor agrega un zoom de 0.03 en las transiciones),SCENE_LIBS=['ways/way_make.js','ways/<film>_beats.js','ways/<film>.js'],SCENE_DIR='ways',ERAS=[{id:'<film>',dur:1}]. NO dibujes texto en el lienzo. Los ayudantes de dibujo (cajas redondeadas, puntos, líneas, desvanecimientos, movimientos a lo largo de una ruta) se escriben por película;examples/pizza/style-a/pizza.jstiene un pequeño conjunto para copiar. Luegorender_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>(o todo a la vez:make.sh). Después de cualquier edición de escena, vuelve a renderizar y ejecutamidmove.pyen los tiempos de movimiento medio: las etiquetas deben estar sobre sus objetos. - B. Copia el motor de la misma manera (con el
favicon.icovacío: en Chrome real, su 404 cuenta como un error de página yrender.pysale con 1 después de escribir un buen video) y parchearender.pyachromium.launch(channel='chrome', …). Ponkit.jsenengine/demos/_hb/, escribeengine/demos/<film>/board.js(usaHB.make(), ancla eventos aBEATS[k].t0 + offset) yengine/demos/<film>/eras.js(FILM_SIZE=[1080,1080],PUNCH=0, fuente Kalam,SCENE_LIBS=['demos/<film>/beats.js','demos/_hb/kit.js','demos/<film>/board.js']). Luegobuild_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, el CRF más bajo que quepa) →test_contract.py final/<film>.html --beats beats.json. Cada película termina con un alejamiento a toda la pizarra, una pausa de ≥ 1 s, luego un borrador; la pizarra vacía es donde se reinicia el bucle. Elstilldel último momento en beats.json es compartido por todos los estilos y debe terminar antes de que comience el borrador, o el póster y el fotograma de movimiento reducido mostrarán una pizarra medio borrada. Los objetos que se mueven (un scooter, una caja) son elementosK.customconhanded: falsecuya posición es una función del tiempo; asigna a cada uno un tiempo final, o se quedará en la pizarra después de su trabajo. - C. Comienza desde
examples/pizza/style-c/(SVG, línea de tiempo, tema). Escribesrc/<film>.svg(un<svg id="scene" class="scene">que contiene<g id="world">; etiqueta cada<text>o su grupo condata-b="<beat>", basado en 1, establecido en el momento en que la cámara lo muestra POR PRIMERA VEZ) ysrc/<film>.js(fotogramas de cámaraFR/ORDER, cada fotograma con un rectwpara pantallas anchas y un recttpara pantallas estrechas; revelaciones conR(selector, beat, offset), movimientos concamTo(i, beat, offset), dibujos de líneas condraw()). En el JS,beatestá basado en 0 (B[0]es el momento 1).R()es solo para palabras, con offset ≥ 2.5 s: cada llamadaR()se registra como una revelación de palabra y el subtítulo cuenta como palabras en el offset 0, por lo queR()en una forma sin palabras o en el offset 0 falla la verificación de espaciado. Las formas, el movimiento y las palabras que llegan con el subtítulo usantl.to(…, T(beat, offset)). Envuelve cualquier elemento que tenga su propiotransformen un<g>padre y anima al hijo. Luegobuild.py beats.json --src src --out final [--css theme.css]→test_contract.py final/<film>.html --stills <scratch>/stills→widthcheck.py. El tiempo de ejecución (shell.html,shell.css,head.js,tail.js) se reutiliza tal cual; para que coincida con la página anfitriona, coloca nuevos tokens:root(--panel,--ink,--muted,--line,--gold,--cap-ink,--on-gold, …) en un archivo de tema y pásalo con--css. Para una imagen de README o chat de una película:gif.py final/<film>.html --beat 4 --from 1.9 --to 8.6 --out film.gif(o--still <s>para un PNG).
- A.
-
Verificar antes de mostrar a nadie (las tres construcciones se saltaron los últimos tres; hazlos):
- compuerta de ritmo OK; todas las pruebas de contrato pasan a 1100 y 360 px en Chrome real; tamaños dentro del presupuesto;
- mirar las hojas de contacto de cada momento en ambos anchos (
scripts/common/contact_sheet.py); huashu film_gate.pyen los mp4 de A/B (con el shim ffprobe si es necesario), rojo leído como se explica entraps.md;- un revisor independiente que no lo construyó ve cada bucle a velocidad normal y escribe hallazgos;
- probar dentro de la página anfitriona real (lado padre de
references/embed-contract.md), en un teléfono real, y en Safari (respaldo de estilo A/B: póster + subtítulos numerados cuando H.264 no puede reproducirse).
-
Entrega. Muestra los tres estilos uno al lado del otro (imágenes fijas o una página de conmutador), con las duraciones de bucle, tamaños, lo que se inventó en la imagen (para que el usuario lo confirme), y lo que no se verificó.
Lado anfitrión: los nueve en una diapositiva con un conmutador (scripts/deck/)
Lo que hace la presentación de ejemplo, y lo que se rompió en el camino:
- Incrustar, no enlazar. Cada película va en la presentación como
<script type="text/plain" id="anim-{style}-{way}">con</script→</scripty<!--→<!--escapados; JS lo decodifica eniframe.srcdocsolo cuando se muestra la diapositiva (un iframe, construido bajo demanda).embed_anims.pyhace esto de forma idempotente entre los marcadores<!--ANIM-DATA-START-->/<!--ANIM-DATA-END-->, por lo que volver a ejecutar después de una reconstrucción reemplaza solo los datos. La presentación creció de ~0.3 MB a 4.5 MB. - Una diapositiva, dos grupos de botones segmentados (Forma, Estilo;
aria-pressed), más un enlace "Míralo funcionar" dentro del panel de detalles de cada Forma que establece la forma, cierra el panel y salta a la diapositiva. - Reproducir/pausar sigue la diapositiva:
go()envía un eventodeck:slide; el conmutador publicaanim-playcuando su diapositiva se vuelve actual (y en la carga del iframe si está actual),anim-pauseen caso contrario. - Gestos sobre el iframe: los manejadores de rueda/tecla/deslizamiento de la presentación se refactorizaron en
onWheel(dy,mode),onKey(key),onSwipe(dy,dx), llamados tanto por los propios oyentes de la presentación como por los mensajes reenviados del iframe (aceptar soloe.source === frame.contentWindow).howtest2.pydemuestra que un gesto = una diapositiva. - Ajustar a una pantalla de portátil: primero estrechar el diseño interno, hacer zoom al final. A 1280×800, las películas tenían 740–840 px de alto a ancho completo, por lo que el subtítulo caía debajo de la pantalla. Lo que funciona (
howFitenembed_anims.py): el iframe srcdoc es del mismo origen, por lo que la presentación mide la altura decontentDocument.bodypor sí misma. Prueba anchos internos desde el ancho de la caja hasta 740 px (búsqueda binaria, esperando ~90 ms por ancho porque una película puede establecer su diseño en JS al cambiar de tamaño, fotograma oculto mientras tanto) y mantiene el más ancho que quepa; solo si 740 px sigue siendo demasiado alto, estableceframe.style.zoom. Estrechar es mejor que hacer zoom porque el subtítulo mantiene su tamaño mientras la imagen se encoge. Problemas encontrados en el camino: (a) zoom conwidth:100%ensancha el diseño interno (1126 → 1876 px) y baja al mínimo, así que fija siempreframe.style.widthen px; (b)documentElement.scrollHeightnunca informa menos que la altura actual del iframe, por lo que no puede impulsar la reducción (medirbodyen su lugar, con el fotograma hecho alto); (c) medir en el mismo tick que el cambio de ancho lee un diseño JS obsoleto. Pide a las películas un diseño lado a lado cuando sean anchas (imágenes cuadradas) y ≤ 580 px de altura total a 780 px de ancho. Los teléfonos (< 900 px) no caben; la diapositiva se desplaza, con un encabezado compacto. - Pruebas:
howtest.py(cambio, enlace profundo, flechas),howtest2.py(los 9 se cargan y reproducen en Chrome real, rueda/tecla/deslizamiento sobre el iframe mueven exactamente una diapositiva), más las comprobaciones de un gesto por diapositiva de la propia presentación. Cualquier prueba de presentación que codifique números de diapositiva se rompe cuando se inserta la diapositiva: buscar diapositivas por id.
Ritmo (la queja principal del usuario sobre el primer corte)
Los primeros prototipos tenían 8–13 s. El veredicto del lector: demasiado rápido para leer lo que sucedía, y necesitaba subtítulos. La regla utilizada para la reconstrucción:
- cada momento dura ≥ max(4.5 s, (palabras del subtítulo + palabras nuevas de la imagen) / 3 + 1.5 s). El recuento canónico es
scripts/common/pace_check.py(ybuild.pydel estilo C, que cuenta de la misma manera desde el SVG). Elpace_check.pypropio del estilo A verifica los tiempos reales de las etiquetas de la película construida y difiere en una cosa: una palabra repetida por una nueva etiqueta en un momento posterior se cuenta nuevamente allí; - algo nuevo aparece solo ≥ 2.5 s después de las palabras anteriores; la cámara se mantiene quieta mientras se leen las palabras;
- movimientos lentos de cámara entre momentos (1.4–2.0 s, interpolación sinusoidal), como máximo UN acento rápido por película, en el veredicto;
- ≥ 1 s de pausa en el último fotograma antes del bucle.
Consecuencia que decir al usuario de antemano: los subtítulos de ~20 palabras por momento hacen películas de 5–6 momentos de 40–80 s. En el ejemplo práctico, los objetivos de 18–34 s del resumen eran imposibles (solo los subtítulos necesitaban 31.5–51.7 s); los tres agentes eligieron la regla de lectura y construyeron bucles de 35–82 s, que nadie había confirmado con el usuario. Los bucles más cortos significan subtítulos más cortos, cambiados una vez en beats.json para todos los estilos.
Contrato de incrustación, tamaño y teléfonos (resumen; texto completo en references/embed-contract.md)
- Un archivo HTML autónomo por película, cargado por el anfitrión en
<iframe srcdoc>. Imagen + tira de subtítulos + riel de pasos + Reproducir/Pausar, nada más. Medios en línea base64;