Manim과 Reveal.js + Typed.js를 장기 생산 관점에서 정밀 비교.
기준
- 5분 내외 롱폼 영상에 적합한 구조적 전개
- LaTeX 수식 중심 콘텐츠의 안정성
- Markdown 자산 재사용성
- 장기적 생산 속도와 유지보수 비용
- “옛날 판서 느낌을 디지털로 깔끔하게” 재현 가능성
1. Manim (Mathematical Animation Engine, 매니메이션 엔진)
핵심 성격
- Python 기반 장면(Scene) 단위 애니메이션 엔진
- LaTeX를 렌더링의 1급 시민으로 취급
- 영상 자체를 목표 산출물로 설계됨
생산 파이프라인
Markdown/LaTeX 개념 정리
→ Manim Scene 스크립트
→ MP4 렌더
장점 (롱폼 + 장기 생산 관점)
- 수식 안정성
- 판서 느낌 구현 가능성
- 영상 품질 일관성
- 장기적 확장성
단점 (생산 속도 관점)
- 초기 비용
- Markdown 직접 호환성 부족
2. Reveal.js + Typed.js
핵심 성격
- 슬라이드 중심 웹 프레젠테이션
- Typed.js로 타이핑 효과 추가
- MathJax로 LaTeX 렌더링
생산 파이프라인
Markdown
→ Reveal.js 슬라이드
→ 브라우저 렌더
→ 화면 녹화 or Puppeteer 캡처
장점 (생산 속도 관점)
- Markdown 친화성
- 초기 생산 속도
- 수정 비용 낮음
단점 (롱폼 영상 관점)
- 수식 애니메이션 한계
- 5분 영상의 구조적 피로
- 최종 영상 품질
핵심 비교 요약
| 항목 | Manim | Reveal.js + Typed.js |
|---|---|---|
| LaTeX 수식 안정성 | 매우 높음 | 중간 |
| 판서 느낌 | 구현 가능 (정밀) | 제한적 |
| Markdown 재사용 | 낮음 (직접 파이프라인 필요) | 매우 높음 |
| 초기 생산 속도 | 느림 | 매우 빠름 |
| 장기 생산 속도 | 빠름 (모듈화 후) | 중간 |
| 5분 롱폼 적합성 | 매우 높음 | 보통 |
| 영상 품질 | 렌더 기반, 안정 | 녹화 기반, 변수 많음 |
장기 생산 기준의 결론
- “지금은 내용이 이미 다 있고, 앞으로 계속 만들 것”
- “5분 롱폼 영상”
- “수식이 주인공이고 판서 느낌이 핵심”
이 조건에서는 Manim이 장기적으로 더 빠른 도구가 된다.
초기에는 Reveal.js가 빨라 보이지만, 10편 이상부터는 Manim의 Scene 재사용성이 압도적으로 유리하다.
현실적인 권장 전략 (혼합)
- 초반 1–2편:
- 이후:
원하면 다음 단계로
- “Markdown → Manim 반자동 파이프라인 설계”
- “판서 느낌을 위한 Manim 애니메이션 템플릿” 까지 구체화해줄 수 있다.
최상의 결과물 기준
- Markdown(마크다운) 원본을 단일 소스로 유지
- LaTeX(라텍스) 수식이 깨지지 않고 Manim(매니메이션 엔진)에서 안정 렌더링
- “판서 느낌” 연출이 템플릿화되어 반복 제작 속도 최대화
- 5분 롱폼에서 장면(Scene) 분할, 타이밍, 내레이션 싱크를 반자동화
- 실패 지점(수식 컴파일, 폰트, 줄바꿈, 길이)을 자동 검증
아래는 내가 추천하는 Manim 기반 반자동 파이프라인 설계안이다. 그대로 구축하면 “Markdown만 고치면 영상이 다시 뽑히는” 흐름까지 간다.
전체 구조
입력
- notes.md: 본문(설명 텍스트 + 수식)
- meta.yml: 장면별 옵션(템포, 카메라, 판서 스타일, 내레이션 구간)
- assets/: 이미지, 참고 도표
변환
- build.py: Markdown 파서 + AST(abstract syntax tree, 추상 구문 트리) 생성
- emit.py: AST → Manim Scene 코드 생성
- render.sh: 렌더(저해상도 프리뷰, 고해상도 최종)
출력
- out/preview.mp4
- out/final.mp4
- out/script.txt (자막/내레이션 스크립트 자동 추출)
Markdown 문법을 “영상 제작용”으로 제한하기
일반 Markdown을 다 지원하려고 하면 오히려 느려진다. 아래 정도로 제한하면 안정성과 자동화가 크게 오른다.
- 장면 구분
- H2(heading level 2, 2단계 제목)
##를 Scene 경계로 사용
- 블록 타입
- 일반 문단: 설명 텍스트
- 수식 블록:
$$ ... $$ - “단계 전개”: 번호 리스트
1. 2. 3.를 “한 줄씩 등장”으로 매핑 - 강조 박스: 커스텀 fenced block
예시 notes.md
## Def: Convexity
Convex function is defined as:
$$
f(\lambda x + (1-\lambda)y) \le \lambda f(x) + (1-\lambda)f(y)
$$
Steps:
1. Fix $x,y$
2. Choose $\lambda\in[0,1]$
3. Compare chord and graph
```chalk
Write the inequality slowly.
Pause after the LHS appears.
AST 설계
최소 노드만 둔다.
- Scene(title, blocks[])
- Paragraph(text)
- DisplayMath(latex)
- Steps(items[])
- ChalkNote(text) # 렌더에는 안 나오고 타이밍/연출 힌트로만 사용
이 AST를 만들면, 이후는 “노드별 렌더러”만 바꾸면 된다.
렌더링 템플릿
장기 생산 속도를 좌우하는 부분이다. 핵심은 “연출 스타일을 코드로 고정”하는 것.
스타일 목표
- 텍스트는 너무 타이핑스럽지 않게, 약간의 손글씨 느낌(등장 애니메이션이 stroke 기반)
- 수식은 Write 계열로 “써지는 느낌”
- 단계는 한 줄씩 등장 + 이전 줄은 약간 옅게(가독성 유지)
Manim 템플릿(핵심만)
from manim import *
class ChalkStyle:
def __init__(self):
self.text_font = "STIX Two Text"
self.math_font = "STIX Two Math"
self.line_spacing = 0.9
self.scale = 0.9
self.write_time = 1.0
self.step_time = 0.6
def mk_paragraph(s: str, style: ChalkStyle):
return Paragraph(
*s.split("\n"),
font=style.text_font,
line_spacing=style.line_spacing
).scale(style.scale)
def mk_math(latex: str, style: ChalkStyle):
# latex는 $$ $$ 제거 후 들어온다고 가정
return MathTex(latex, font_size=42)
def anim_write(mobj, style: ChalkStyle):
return Write(mobj, run_time=style.write_time)
판서 느낌을 더 올리고 싶으면 “stroke reveal”을 강화하는 방식으로 간다. 다만 LaTeX의 glyph를 진짜 필기 경로로 바꾸는 건 비용이 커서, 장기 생산 목적이면 Write/Create 조합 + 약간의 jitter(위치/시간 미세 흔들림) 정도가 현실적인 최적점이다.
Markdown → AST 파서
Python(파이썬)에서 markdown-it-py 또는 mistune 같은 파서로 토큰화한 뒤, 위에서 정한 “지원 문법만” AST로 만든다.
간단 파서 스케치(핵심 로직만)
import re
from dataclasses import dataclass
@dataclass
class Scene: title: str; blocks: list
@dataclass
class Paragraph: text: str
@dataclass
class DisplayMath: latex: str
@dataclass
class Steps: items: list
@dataclass
class ChalkNote: text: str
def parse(md: str) -> list[Scene]:
scenes = []
cur = None
buf = []
def flush_paragraph():
nonlocal buf, cur
if not buf: return
text = "\n".join(buf).strip()
if text:
cur.blocks.append(Paragraph(text))
buf = []
lines = md.splitlines()
i = 0
while i < len(lines):
line = lines[i]
m = re.match(r"^##\s+(.*)$", line)
if m:
if cur:
flush_paragraph()
scenes.append(cur)
cur = Scene(title=m.group(1).strip(), blocks=[])
i += 1
continue
if line.strip().startswith("```chalk"):
flush_paragraph()
i += 1
note_lines = []
while i < len(lines) and not lines[i].strip().startswith("```"):
note_lines.append(lines[i])
i += 1
cur.blocks.append(ChalkNote("\n".join(note_lines).strip()))
i += 1
continue
if line.strip() == "$$":
flush_paragraph()
i += 1
math_lines = []
while i < len(lines) and lines[i].strip() != "$$":
math_lines.append(lines[i])
i += 1
cur.blocks.append(DisplayMath("\n".join(math_lines).strip()))
i += 1
continue
if re.match(r"^\d+\.\s+", line):
flush_paragraph()
items = []
while i < len(lines) and re.match(r"^\d+\.\s+", lines[i]):
items.append(re.sub(r"^\d+\.\s+", "", lines[i]).strip())
i += 1
cur.blocks.append(Steps(items))
continue
buf.append(line)
i += 1
if cur:
flush_paragraph()
scenes.append(cur)
return scenes
AST → Manim Scene 코드 생성
방법 A: 한 파일에 “자동 생성 Scene 클래스”들을 생성
방법 B: 하나의 GenericScene이 AST를 읽어서 런타임에 렌더
장기 생산에서는 B가 좋다. 즉 “코드를 생성하지 않고 데이터만 바꿔서 렌더”.
예: JSON(제이슨)으로 AST 덤프 후, Manim에서 읽기.
build.py
- notes.md → scenes.json
manim_project/video.py
- scenes.json 읽어서 순서대로 렌더
GenericScene 스케치
import json
from manim import *
class Video(Scene):
def construct(self):
data = json.load(open("out/scenes.json", "r", encoding="utf-8"))
style = ChalkStyle()
for sc in data:
self.render_scene(sc, style)
self.clear()
def render_scene(self, sc, style):
title = Text(sc["title"], font=style.text_font).scale(0.8).to_edge(UP)
self.play(FadeIn(title, shift=DOWN), run_time=0.4)
y = 2.6
for blk in sc["blocks"]:
t = blk["type"]
if t == "Paragraph":
m = mk_paragraph(blk["text"], style).to_corner(UL).shift(DOWN*0.6)
self.play(FadeIn(m, shift=UP*0.1), run_time=0.35)
elif t == "DisplayMath":
m = mk_math(blk["latex"], style).next_to(title, DOWN, buff=0.6)
self.play(anim_write(m, style))
elif t == "Steps":
self.render_steps(blk["items"], title, style)
elif t == "ChalkNote":
# 렌더 없음: 타이밍 힌트로만 사용
pass
def render_steps(self, items, anchor, style):
group = VGroup()
for k, it in enumerate(items):
line = Tex(rf"{k+1}.\;\; {it}")
group.add(line)
group.arrange(DOWN, aligned_edge=LEFT, buff=0.25).next_to(anchor, DOWN, buff=0.8).to_edge(LEFT)
for k, line in enumerate(group):
self.play(Write(line), run_time=style.step_time)
if k >= 1:
group[k-1].set_opacity(0.55)
타이밍과 내레이션 싱크 반자동화
meta.yml을 둬서 Scene별로 템포를 바꾼다.
meta.yml 예시
default:
write_time: 1.0
step_time: 0.6
scenes:
- title: "Def: Convexity"
write_time: 1.2
pause_after_math: 0.5
ChalkNote에 “Pause after …” 같은 힌트를 넣어두고, 파서가 이를 meta 이벤트로 변환해 self.wait()를 자동 삽입하면 제작 속도가 올라간다.
자동 검증(실패를 빨리 발견)
- LaTeX 컴파일 검증
- scenes.json 생성 단계에서 MathTex용 latex를 한 번 “dry run” 검사(가능하면)
- 최소로는
$짝,\begin{}짝, 금지 문자 체크
- 길이 검증
- 문단이 너무 길면 자동으로 Scene을 쪼개거나 경고 출력
- 렌더 프리뷰
- 저해상도(예: 480p, low quality)로 빠르게 전체를 뽑고, 최종만 high quality
render.sh 예시
python build.py notes.md meta.yml
manim -pql manim_project/video.py Video
# 최종
manim -pqh manim_project/video.py Video
운영 팁: 생산 속도를 올리는 설계 선택
- 스타일을 바꾸고 싶을 때, Markdown을 고치지 말고 ChalkStyle만 고치게 한다.
- “한 Scene = 하나의 개념 단위”로 자른다. 5분이면 6~12개 Scene이 적당하다.
- Steps를 적극 사용한다. 수식이 많아도 Steps가 있으면 판서 전개가 자동으로 자연스러워진다.
- 이미 있는 Markdown을 “영상용 Markdown”으로 최소 수정해서 유지한다.
자체 검증
설정한 기준 1~5에 대해 점검
- Markdown 단일 소스: 가능(notes.md 중심)
- LaTeX 안정성: MathTex 기반으로 만족
- 판서 느낌: Write/Create 중심으로 템플릿화 가능
- 장기 생산 속도: GenericScene + JSON AST로 매우 높음
- 자동 검증: 빌드 단계 경고/프리뷰 렌더로 만족
원하면, 다음을 바로 제공할 수 있다.
- 위 구조를 실제 폴더로 구성한 최소 동작 예제(파일 5~7개)
- “수식 변형 중심” 템플릿(TransformMatchingTex 기반)과 “판서 전개 중심” 템플릿 두 종류