wiggle-that-diagram: 3가지 스타일의 설명 모션은(는) 무엇을 하나요?
정적인 '작동 방식' 일러스트레이션을 짧고, 자막이 있는 루핑 애니메이션으로 변환하여 HTML 데크나 보고서에 삽입하고, 세 가지 형식으로 제공하여 독자가 선택할 수 있도록 합니다. 이 방법은 실제 고객 데크를 통해 개발되었습니다(참고: "실제 예시"). huashu-art-motion을 방향 설정(샷 목록, 카메라 규칙, 실패하는 6가지 방법)과 스타일 A 및 B의 렌더링 엔진으로 사용합니다.
사용 시기와 사용하지 말아야 할 시기
다음과 같은 경우에 사용하세요:
- 데크나 HTML 보고서에 메커니즘 다이어그램(파일 흐름, 규칙 검사, 승인)이 있고, 독자가 단계별로 실제로 작동하는 모습을 읽을 수 있는 텍스트와 함께 봐야 하는 경우
- 사용자가 단순히 세 가지 색상 구성표가 아닌, 진정으로 다른 형식의 옵션을 원하는 경우
- 이전 애니메이션이 "너무 빨라서 읽을 수 없었거나" 자막이 필요한 경우
다음과 같은 경우에는 사용하지 마세요: 단일 마이크로 인터랙션(animate 사용), 내레이션이 포함된 비디오(huashu-art-motion 직접 사용), 숫자 차트(dataviz 사용).
세 가지 스타일 (팔레트가 아닌 형식에서 차이가 있습니다)
| A · 다이어그램 클립 | B · 화이트보드 | C · 라이브 SVG | |
|---|---|---|---|
| 그림 | huashu 엔진 캔버스 장면, t1_3b1b 문법, 텍스트 없는 H.264로 렌더링 | huashu y3_whiteboard 문법, 어두운 보드, 펜이 그리고 손글씨(Kalam), 정사각형 비디오 | 가상 카메라(viewBox가 GSAP 3.12.5로 트위닝됨)로 촬영된 인라인 SVG 월드 |
| 그림 위의 텍스트 | 덤프된 카메라 트랙에서 비디오 위에 배치된 HTML 레이블(항상 선명함) | 비디오에 그려짐(휴대폰에서도 읽을 수 있을 정도로 커야 함) | 실제 SVG 텍스트(선명하며, 카메라 프레임을 너비에 따라 변경하여 리플로우됨) |
| 영화당 크기(실제 예시) | 360–820KB (예산 900KB) | 847–886KB (예산 900KB) | 21–34KB (예산 40KB) |
| 느낌 | 깔끔한 3Blue1Brown 스타일 다이어그램 | 화이트보드에서 설명하는 누군가 | 정교한 제품 애니메이션 |
| 주요 위험 요소 | 장면이 재렌더링 없이 변경되면 레이블이 어긋남 | 빡빡한 크기 예산; 휴대폰 레이블 12–14px | 까다로운 카메라 수학; GSAP 변환 문제 |
세 가지 모두 하나의 캡션 스크립트, 동일한 비트(beat), 동일한 임베드 계약을 공유하므로, 스타일 전환기를 사용하면 동일한 이야기가 세 가지 방식으로 전달되는 것을 보여줄 수 있습니다.
방법, 처음부터 끝까지
각 단계는 완료되었음을 증명하는 검사(check)로 끝납니다.
-
메커니즘과 구체적인 예시 하나를 찾으세요. 호스트 페이지를 읽습니다(읽기 전용). 표시할 순간과 모든 비트(beat)에 걸쳐 연결될 하나의 실제 예시(한 행, 한 레코드, 한 사례, 실제 값 포함)를 선택하세요. 발명해서는 안 되는 것(id, 수치, 이미지)을 목록으로 만드세요. → 확인: 각 비트(beat)를 "이 객체가 이것을 한다"라고 명사와 동사로 말할 수 있어야 합니다.
-
캡션 스크립트를 먼저 작성하세요. 영화당 비트(beat)당 캡션 문장 하나씩, 3–6비트,
beats.json파일에 작성합니다(형식:scripts/common/beats.example.json). 캡션은 모든 스타일에서 동일합니다. 그런 다음 페이싱 게이트(pacing gate)를 실행하고 출력된 기준을 읽습니다:python3 ~/.claude/skills/wiggle-that-diagram/scripts/common/pace_check.py beats.json→ 확인: 모든 비트(beat)가 적절하고, 사용자가 결과 루프 길이(또는 더 짧은 캡션)에 동의했습니다. 캡션과 별도로 루프 대상을 설정하지 마십시오. "페이싱"을 참조하세요.
-
브리프(brief)를 작성하세요.
references/brief-template.md를SPEC.md로 채웁니다: 인용된 피드백, 토큰, 페이싱 규칙, 캡션 스크립트, 비트(beat), 그대로 포함된 임베드 계약, 예산, 결과물. → 확인: 다른 맥락이 없는 에이전트도 이 정보만으로 빌드할 수 있어야 합니다. -
방향 설정 라운드(선택 사항, 사용자가 스타일을 보지 못한 경우). 세 에이전트가 병렬로, 각각 한 스타일을 담당하며, 동일한 영화의 한 순간(~8–12초)을 프로토타이핑하고 1280px 및 390px에서 스틸 이미지를 촬영합니다. 스틸 이미지를 나란히 보여주고, 사용자가 하나를 선택하거나 세 개를 모두 유지합니다. → 확인: 사용자의 선택이 기록에 남습니다.
-
빌드 라운드. 스타일당 하나의 에이전트(각자 자신의 프로토타입을 이어받음), 해당 스타일의 모든 영화를
<work>/<style>/final/아래에만 작성합니다. 브리프:references/agent-briefs.md. 실행하기 전에 에이전트 수와 티어를 사용자에게 알리세요. → 확인: 각각 파일, 스틸 이미지, NOTES.md 및 정직한 미해결 문제 목록을 반환합니다. -
빌드 레시피(각 스타일 에이전트가 실제로 한 일; 스크립트는
scripts/style-a|b|c/에 있습니다): 두 개의 중립적인 영화가 세 가지 스타일 모두로 제공되며, 각각beats.json과style-a/,style-b/,style-c/폴더로 구성됩니다:examples/pizza/(5비트, 단순함) 및examples/expenses/(6비트: 카드, 색상별 이유, 두 번째 및 세 번째 패널로의 화살표). 해당 스타일의 폴더에서 시작하세요.- A.
cp -R ~/.claude/skills/huashu-art-motion/scripts/engine <A>/engine및touch <A>/engine/favicon.ico.engine/ways/way_make.js추가(scripts/style-a/에서),beats_js.py beats.json --engine engine --var <VAR>실행(캡션 및 타이밍은 공유 스크립트에서 가져오며, 수동 복사하지 않음),WAY.make({poster, beats, camBase, cam, labels})를 호출하고SCENES['<film>']를 등록하는 장면 파일engine/ways/<film>.js, 및FILM_SIZE=[1280,720],PUNCH=0(그렇지 않으면 엔진이 전환에 0.03 확대 효과를 추가함),SCENE_LIBS=['ways/way_make.js','ways/<film>_beats.js','ways/<film>.js'],SCENE_DIR='ways',ERAS=[{id:'<film>',dur:1}]를 설정하는engine/eras_<film>.js. 캔버스에 텍스트를 그리지 마십시오. 그리기 도우미(둥근 상자, 점, 선, 페이드, 경로를 따른 이동)는 영화별로 작성됩니다.examples/pizza/style-a/pizza.js에 복사할 수 있는 작은 세트가 있습니다. 그런 다음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>(또는 한 번에:make.sh). 장면을 편집한 후에는 다시 렌더링하고 중간 이동 시점에midmove.py를 실행하세요. 레이블이 해당 객체 위에 있어야 합니다. - B. 동일한 방법으로 엔진을 복사하고(빈
favicon.ico사용: 실제 Chrome에서는 404가 페이지 오류로 간주되어render.py가 좋은 비디오를 작성한 후 종료 코드 1로 종료됨),render.py를chromium.launch(channel='chrome', …)로 패치하세요.kit.js를engine/demos/_hb/에 넣고,engine/demos/<film>/board.js작성(HB.make()사용,BEATS[k].t0 + offset에 이벤트 연결) 및engine/demos/<film>/eras.js작성(FILM_SIZE=[1080,1080],PUNCH=0, Kalam 글꼴,SCENE_LIBS=['demos/<film>/beats.js','demos/_hb/kit.js','demos/<film>/board.js']). 그런 다음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², 24fps, 맞는 가장 낮은 CRF) →test_contract.py final/<film>.html --beats beats.json. 각 영화는 전체 보드로의 후퇴, ≥ 1초 유지, 그 다음 지우개로 지우기로 끝납니다. 빈 보드는 루프가 다시 시작되는 곳입니다. beats.json의 마지막 비트(beat)의still은 모든 스타일에서 공유되며 지우개 시작 전에 도달해야 합니다. 그렇지 않으면 포스터와 축소 모션 프레임이 반쯤 지워진 보드를 보여줍니다. 움직이는 객체(스쿠터, 상자)는handed: false인K.custom항목이며 위치는 시간의 함수입니다. 각 객체에 종료 시간을 지정하세요. 그렇지 않으면 작업이 완료된 후에도 보드에 남아 있습니다. - C.
examples/pizza/style-c/에서 시작합니다(SVG, 타임라인, 테마).src/<film>.svg작성(하나의<svg id="scene" class="scene">는<g id="world">를 포함하며, 모든<text>또는 해당 그룹에data-b="<beat>"태그를 지정하고, 1부터 시작하여 카메라가 처음 표시하는 비트(beat)로 설정합니다) 및src/<film>.js작성(카메라 프레임FR/ORDER, 각 프레임은 와이드 스크린용wrect와 좁은 스크린용trect를 가짐;R(selector, beat, offset)로 공개,camTo(i, beat, offset)로 이동,draw()로 선 그리기). JS에서beat는 0부터 시작합니다(B[0]은 비트 1).R()은 오프셋 ≥ 2.5s에서만 단어에 사용됩니다. 모든R()호출은 단어 공개로 기록되며 캡션은 오프셋 0에서 단어로 계산되므로, 단어 없는 모양이나 오프셋 0에서의R()은 간격 검사에 실패합니다. 캡션과 함께 도착하는 모양, 움직임 및 단어는 일반tl.to(…, T(beat, offset))를 사용합니다. 자체transform이 있는 모든 요소는 상위<g>로 감싸고 하위 요소를 애니메이션 처리하세요. 그런 다음build.py beats.json --src src --out final [--css theme.css]→test_contract.py final/<film>.html --stills <scratch>/stills→widthcheck.py. 런타임(shell.html,shell.css,head.js,tail.js)은 그대로 재사용됩니다. 호스트 페이지와 일치시키려면:root토큰(--panel,--ink,--muted,--line,--gold,--cap-ink,--on-gold, …)을 테마 파일에 넣고--css로 전달하세요. 영화의 README 또는 채팅 사진용:gif.py final/<film>.html --beat 4 --from 1.9 --to 8.6 --out film.gif(또는 PNG용--still <s>).
- A.
-
다른 사람에게 보여주기 전에 확인하세요(세 가지 빌드 모두 마지막 세 단계를 건너뛰었습니다. 지금 수행하세요):
- 페이싱 게이트 통과; 계약 테스트가 실제 Chrome에서 1100px 및 360px에서 모두 통과; 크기가 예산 미만;
- 두 너비에서 모든 비트(beat)의 연락처 시트를 확인합니다(
scripts/common/contact_sheet.py); - A/B mp4에 대해 huashu
film_gate.py실행(필요시 ffprobe shim 사용),traps.md에 설명된 대로 빨간색 읽기; - 빌드하지 않은 독립적인 검토자가 각 루프를 정상 속도로 시청하고 소견을 작성합니다;
- 실제 호스트 페이지(
references/embed-contract.md의 상위 측), 실제 휴대폰, Safari에서 테스트합니다(스타일 A/B 폴백: H.264를 재생할 수 없을 때 포스터 + 번호가 매겨진 캡션).
-
인계. 세 가지 스타일을 나란히(스틸 이미지 또는 전환기 페이지) 루프 길이, 크기, 그림에 발명된 내용(사용자 확인용), 확인되지 않은 내용과 함께 보여줍니다.
호스트 측: 전환기가 있는 하나의 슬라이드에 9개 모두 (scripts/deck/)
실제 데크가 한 일과 그 과정에서 발생한 문제:
- 링크가 아닌 임베드. 각 영화는
<script type="text/plain" id="anim-{style}-{way}">로 데크에 들어가며</script→\</script및<!--→\<!--로 이스케이프됩니다. JS는 슬라이드가 표시될 때만(주문형으로 빌드된 하나의 iframe) 이를iframe.srcdoc으로 디코딩합니다.embed_anims.py는<!--ANIM-DATA-START-->/<!--ANIM-DATA-END-->마커 사이에서 이 작업을 멱등적으로 수행하므로, 재빌드 후 다시 실행하면 데이터만 교체됩니다. 데크 크기는 ~0.3MB에서 4.5MB로 증가했습니다. - 하나의 슬라이드, 두 개의 분할 버튼 그룹(Way, Style;
aria-pressed), 각 Way의 세부 패널 내부에 있는 "작동 보기" 링크(way를 설정하고 패널을 닫은 후 슬라이드로 이동). - 재생/일시 정지는 슬라이드를 따릅니다:
go()는deck:slide이벤트를 디스패치합니다. 전환기는 슬라이드가 현재 슬라이드가 될 때anim-play를 게시하고(현재 슬라이드인 경우 iframe 로드 시에도), 그렇지 않으면anim-pause를 게시합니다. - iframe 위의 제스처: 데크의 휠/키/스와이프 핸들러는
onWheel(dy,mode),onKey(key),onSwipe(dy,dx)로 리팩토링되었으며, 데크 자체 리스너와 iframe의 전달된 메시지 모두에서 호출됩니다(e.source === frame.contentWindow만 허용).howtest2.py는 하나의 제스처 = 하나의 슬라이드를 증명합니다. - 하나의 노트북 화면에 맞추기: 먼저 내부 레이아웃을 좁히고, 마지막으로 확대/축소합니다. 1280×800에서 영화는 전체 너비에서 740–840px 높이였으므로 캡션이 화면 아래로 떨어졌습니다. 작동하는 방법(
embed_anims.py의howFit): srcdoc iframe은 동일 출처이므로 데크가contentDocument.body높이를 직접 측정할 수 있습니다. 상자 너비에서 740px까지 내부 너비를 시도하고(이진 검색, 영화가 크기 조정 시 JS에서 레이아웃을 설정할 수 있으므로 너비당 ~90ms 대기, 그동안 프레임 숨김), 맞는 가장 넓은 너비를 유지합니다. 740px도 너무 높은 경우에만frame.style.zoom을 설정합니다. 좁히기가 확대/축소보다 나은 이유는 캡션 크기를 유지하면서 그림이 줄어들기 때문입니다. 그 과정에서 만난 함정: (a)width:100%로 확대/축소하면 내부 레이아웃이 넓어지고(1126 → 1876px) 바닥까지 반복되므로 항상frame.style.width를 px로 고정합니다. (b)documentElement.scrollHeight는 iframe의 현재 높이보다 작게 보고되지 않으므로 축소를 유도할 수 없습니다(대신body측정, 프레임을 충분히 높게 만든 상태에서). (c) 너비 변경과 동일한 틱에서 측정하면 오래된 JS 레이아웃을 읽습니다. 넓을 때 나란히 배치(정사각형 그림)하고 780px 너비에서 총 높이 ≤ 580px을 영화에 요청하세요. 휴대폰(<900px)은 맞지 않습니다. 슬라이드는 컴팩트 헤더와 함께 스크롤됩니다. - 테스트:
howtest.py(전환, 딥 링크, 화살표),howtest2.py(9개 모두 실제 Chrome에서 로드 및 재생, iframe 위의 휠/키/스와이프가 정확히 하나의 슬라이드 이동), 데크 자체의 one-gesture-one-slide 검사. 슬라이드 번호를 하드코딩하는 데크 테스트는 슬라이드가 삽입될 때 깨집니다. ID로 슬라이드를 찾으세요.
페이싱 (첫 번째 컷에 대한 사용자의 주요 불만)
첫 번째 프로토타입은 8–13초였습니다. 독자의 평가: 무슨 일이 일어나고 있는지 읽기에는 너무 빠르며 자막이 필요했습니다. 재구축에 사용된 규칙:
- 각 비트(beat)는 최소 max(4.5초, (캡션 단어 수 + 새 그림 단어 수) / 3 + 1.5초) 동안 지속됩니다. 표준 계산은
scripts/common/pace_check.py(및 스타일 C의build.py, SVG에서 동일한 방식으로 계산)입니다. 스타일 A 자체의pace_check.py는 빌드된 영화의 실제 레이블 시간을 확인하고 한 가지 차이점이 있습니다. 이후 비트(beat)에서 새 레이블로 반복된 단어는 다시 계산됩니다. - 새로운 것은 이전 단어가 나온 후 ≥ 2.5초 후에만 나타납니다. 카메라는 단어를 읽는 동안 정지 상태를 유지합니다.
- 비트(beat) 사이의 느린 카메라 이동(1.4–2.0초, 사인 이징), 영화당 최대 하나의 빠른 악센트, 판결 시점에.
- 루프 전 마지막 프레임에서 ≥ 1초 유지.
사용자에게 미리 알려야 할 결과: 비트(beat)당 ~20단어의 캡션은 5–6비트 영화를 40–80초로 만듭니다. 실제 예시에서 브리프의 18–34초 목표는 불가능했습니다(캡션만으로 31.5–51.7초 필요). 세 에이전트 모두 읽기 규칙을 선택하고 35–82초 루프를 구축했으며, 이는 사용자와 확인되지 않았습니다. 더 짧은 루프는 더 짧은 캡션을 의미하며, beats.json에서 한 번 변경하면 모든 스타일에 적용됩니다.
임베드 계약, 크기 및 휴대폰 (요약; 전체 텍스트는 references/embed-contract.md 참조)
- 영화당 하나의 자체 포함 HTML 파일, 호스트가
<iframe srcdoc>에 로드합니다. 그림 + 캡션 스트립 + 단계 레일 + 재생/일시 정지, 그 외에는 없습니다. 미디어는 base64 인라인입니다.