AnimationSpec v2 사양서 — 나만의 "문자 움직임" 만들기
이 문서는 일본어 원문의 번역본입니다. 일본어판이 정본이며, 내용에 차이가 있을 경우 일본어판이 우선합니다.
이 문서는 movlyric의 문자 움직임(가사 애니메이션) 정의인 "AnimationSpec v2"의 완전한 사양입니다. 앱의 "스타일 에디터 → 「움직임」 탭 → 문자 움직임 → 「사용자 지정」 → AI로 만들기…"에서 사용합니다(배경에 도형을 그리는 "배경 이펙트"는 별개이며, 그쪽은 GRAPHIC_SPEC(배경 이펙트)가 사양입니다). v1(모션만 있던 버전)으로 작성된 JSON은 그대로 v2로도 유효합니다. v2에서는 색상·테두리·발광·축별 신축이 추가되었습니다. 이 문서만 읽은 AI(또는 사람)가 앱에 그대로 붙여 넣어 통과하는 JSON을 작성할 수 있게 하는 것이 목적입니다. 문서 끝에 AI에게 전달할 프롬프트 템플릿이 있습니다. 이 문서 전체를 프롬프트의 일부로 붙여 넣어도 작동합니다.
1. 개요
movlyric은 가사(구절)를 시간에 맞춰 화면에 표시하는 동영상 엔진입니다. 하나의 구절은 다음 3단계로 움직입니다.
- 입장(in): 구절 표시 창의 시작(구절 발성 시작 약 1초 전)부터, 문자/단어가 나타나는 움직임
- 표시 중(정지 상태): 입장이 끝난 상태로 정지. 필요하면 activePulse(sin파 맥동)를 걸 수 있음
- 퇴장(out): 표시 창의 끝(구절 발성 종료 약 1초 후)을 향해 사라지는 움직임
AnimationSpec은 이 in / out / activePulse를 keyframe 기반의 선언적 JSON으로 기술합니다. 코드나 수식은 쓸 수 없습니다. 쓸 수 있는 것은 아래 사양에 있는 필드뿐입니다.
2. JSON 형식의 완전한 사양
2.1 루트: AnimationSpec
{ "in": <Phase>, "out": <Phase>, "activePulse": <Pulse> }
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
in |
Phase | 필수 | 입장 페이즈 |
out |
Phase | 필수 | 퇴장 페이즈(퇴장 연출이 필요 없어도 생략 불가. 변화 없이 두고 싶다면 §2.7 참고) |
activePulse |
Pulse | 선택 | 표시 중의 맥동. 필요 없으면 키 자체를 생략(null은 불가) |
루트 바로 아래에 쓸 수 있는 키는 이 3개뿐입니다.
2.2 Phase(in / out 공통)
| 필드 | 타입 | 필수 | 값 범위 | 설명 |
|---|---|---|---|---|
durationMs |
정수 | 필수 | 50~4000 | 유닛(문자/단어) 1개의 페이즈 소요 시간(밀리초). 소수 불가 |
easing |
문자열 | 필수 | §3의 31종 중 하나 | 페이즈 전체에 한 번 적용되는 이징(keyframe 구간마다 지정할 수 없음) |
stagger |
Stagger | 필수 | — | 유닛 간의 시간차. 시간차가 없어도 {"unit":"none","delayMs":0,"from":"start"}처럼 3개 필드 모두 필수 |
keyframes |
Keyframe[] | 필수 | 2~8개 | 움직임의 나열. t는 0..1의 진성 오름차순(같은 값도 불가), 첫 항목은 반드시 t=0, 마지막 항목은 반드시 t=1 |
2.3 Keyframe
| 필드 | 타입 | 필수 | 값 범위 | 의미·단위 |
|---|---|---|---|---|
t |
숫자 | 필수 | 0~1 | 페이즈 내 정규화 시각(0=시작, 1=종료) |
alpha |
숫자 | 선택 | 0~1 | 불투명도(0=투명, 1=불투명) |
scale |
숫자 | 선택 | 0.1~3 | 확대율(1=등배) |
dx |
숫자 | 선택 | -2~2 | 가로 오프셋. 단위는 글자 크기(fontSize) 비율(1.0=글자 1개분). 양수=오른쪽 |
dy |
숫자 | 선택 | -2~2 | 세로 오프셋. 단위는 글자 크기 비율. 양수=아래(예: 아래에서 입장은 양수→0) |
rotate |
숫자 | 선택 | -360~360 | 회전. 단위는 도. 양수=시계 방향 |
scaleX |
숫자 | 선택 | 0.1~3 | 가로만의 확대율. scale과 곱해짐 |
scaleY |
숫자 | 선택 | 0.1~3 | 세로만의 확대율. scale과 곱해짐 |
tint |
정수 | 선택 | 0~16777215 | 글자 색상(0xRRGGBB의 10진 또는 16진 표기) |
strokeWidth |
숫자 | 선택 | 0~20 | 테두리 두께(px). 0이면 테두리 없음 |
strokeColor |
정수 | 선택 | 0~16777215 | 테두리 색상 |
glow |
숫자 | 선택 | 0~40 | 발광의 강도(흐림 반경 px). 0이면 발광 없음 |
glowColor |
정수 | 선택 | 0~16777215 | 발광 색상. 생략하면 글자 색상으로 빛남(자체 발광) |
쓸 수 있는 속성은 위 13개뿐입니다. 다른 이름(x, y, opacity, color, blur, shadow 등)은 존재하지 않습니다.
단위(dx/dy=글자 크기 비율, rotate=도)는 앱 내 애니메이션 에디터의 표시 단위(가로/세로=글자 크기 비율, 회전=도)와 동일합니다.
"지정하지 않음"의 의미는 속성마다 다릅니다. 이를 혼동하면 의도하지 않은 모습이 됩니다.
| 속성 | 지정하지 않았을 때 |
|---|---|
alpha / scale / scaleX / scaleY |
1(변화 없음) |
dx / dy / rotate |
0(변화 없음) |
tint |
글자 색상을 바꾸지 않음(스타일의 가라오케 발성 색상이 그대로 적용됨) |
strokeWidth / strokeColor |
스타일의 테두리를 그대로 사용(0이 되지 않음) |
glow / glowColor |
발광 없음 |
즉 색상이나 테두리를 바꾸고 싶지 않다면 해당 속성을 쓰지 마세요. tint를 쓰면 그 글자는 노래에 맞춘 색 전환(미발성→발성 완료)을 하지 않게 되고, 지정한 색으로 고정됩니다.
scaleX와 scaleY를 따로 움직이면 찌그러졌다 늘어나는 움직임(squash & stretch)을 만들 수 있습니다. 튀어 오르거나 착지하는 표현에서 이것이 있으면 움직임이 부드러워집니다.
2.4 Stagger(시간차)
| 필드 | 타입 | 필수 | 값 범위 | 설명 |
|---|---|---|---|---|
unit |
문자열 | 필수 | "none" / "char" / "word" |
시간차의 단위. none=구절 전체가 동시에, char=글자마다, word=단어마다 |
delayMs |
정수 | 필수 | 0~500 | 유닛 1개분의 지연(밀리초). 소수 불가. unit이 none이라도 0을 명시 |
from |
문자열 | 필수 | "start" / "end" / "center" |
기준점. start=처음부터, end=끝에서부터, center=중앙에서 바깥으로(짝수 개는 중앙 2개가 동시에 가장 먼저) |
n번째로 움직이는 유닛의 지연은 순서×delayMs입니다. 루비(후리가나)는 항상 부모 단어와 같은 위상으로 움직입니다.
in과 out에 서로 다른 stagger를 지정할 수 있습니다(예: 입장은 처음부터, 퇴장은 끝에서부터).
2.5 Pulse(activePulse, 선택)
| 필드 | 타입 | 필수 | 값 범위 | 설명 |
|---|---|---|---|---|
property |
문자열 | 필수 | "scale" / "alpha" |
흔들 대상 |
amplitude |
숫자 | 필수 | 0~0.5 | 흔들림 폭 |
periodMs |
정수 | 필수 | 200~4000 | 주기(밀리초). 소수 불가 |
동작: 각 유닛의 입장 완료 후~퇴장 시작 전까지만, 1 + amplitude × sin(2π × nowMs / periodMs)를 property에 곱합니다.
nowMs는 영상의 절대 시각이므로, 맥동의 위상은 구절을 넘나들며 연속됩니다(결정적 = 미리보기와 내보내기가 일치).
주의: property:"alpha"에서 정지 상태의 alpha가 1일 때, 1을 초과하는 반주기는 클램프되므로 "어두워지는 방향만" 보이게 됩니다(명멸 표현으로서는 그것이 자연스럽습니다).
2.6 보간의 의미론(성긴 keyframe)
- 속성마다 독립적으로 보간합니다. 어떤 속성은 "그 속성을 가진 keyframe"만 이어서 선형 보간합니다.
- 예:
alpha는 t=0과 t=1에만 쓰고,scale은 t=0 / 0.5 / 1에 쓰는 식으로 성기게 쓸 수 있습니다.
- 예:
- 해당 속성을 가진 첫 keyframe보다 앞은 첫 값을, 마지막 keyframe보다 뒤는 마지막 값을 유지합니다(최근접값 유지. 외삽은 하지 않음).
- 어떤 keyframe에도 쓰여 있지 않은 속성의 처리는 §2.3의 "지정하지 않았을 때" 표를 따릅니다. 모션 계열은 항등값(alpha=1, scale=1, scaleX=1, scaleY=1, dx=0, dy=0, rotate=0)이지만, 색상·테두리는 "덮어쓰지 않는다"(스타일의 값이 그대로 남는다)는 점이 다릅니다.
- easing은 "페이즈의 진행 0→1"에 한 번만 적용되며, 그 eased 값으로 모든 속성을 보간합니다.
2.7 in과 out의 합성·정지 상태
in과 out은 항상 동시에 평가되며, alpha와 scale은 곱셈, dx / dy / rotate는 덧셈으로 합성됩니다. 따라서:
- in의 t=1 = 표시 중의 정지 상태입니다. 보통은 항등값(alpha 1, scale 1, dx 0, dy 0, rotate 0)으로 둡니다.
- out의 t=0은 반드시 항등값으로 하세요. 그렇지 않으면 표시 중의 모습이 out의 값만큼 계속 어긋납니다.
- 퇴장 연출을 없애고 싶다면 out을 "변화 없음"으로 만듭니다:
keyframes를[{"t":0,"alpha":1},{"t":1,"alpha":1}]처럼 같은 값으로 쓰면 움직이지 않습니다(t는 오름차순을 지킬 것).
합성 후의 alpha는 최종적으로 0..1로 클램프됩니다.
2.8 타임라인과 짧은 구절에서의 압축
- 표시 창은 구절 발성 구간의 앞뒤에 각각 약 1000ms의 여백을 가집니다. in은 창의 시작부터 시작되고, out은 "마지막 유닛의 t=1이 정확히 창의 끝"이 되도록 역산하여 배치됩니다.
- 페이즈 전체의 소요 시간은
durationMs + 최대 stagger 지연(순서 최댓값×delayMs)입니다. 이것이 표시 창보다 길면 타임라인 전체가 등배로 압축됩니다(창이 짧아도 글자가 나타나지 않은 채로 남지 않도록). 글자 수가 많은 가사에서charstagger에 큰 delayMs를 쓰면 압축되기 쉬우므로,durationMs + 글자수×delayMs가 2000ms 정도에 들어오는 값을 권장합니다.
3. 이징 목록(31종)
easing에 쓸 수 있는 이름은 다음 31종뿐입니다(대소문자도 엄밀히 일치해야 함. ease-in이나 easeOutBack 같은 다른 표기는 불가).
| 이름 | 설명 |
|---|---|
linear |
등속 |
inQuad / outQuad / inOutQuad |
2차. 완만한 가속/감속/양쪽 |
inCubic / outCubic / inOutCubic |
3차. 표준적인 가속/감속/양쪽 |
inQuart / outQuart / inOutQuart |
4차. 강한 가속/감속/양쪽 |
inQuint / outQuint / inOutQuint |
5차. 꽤 강한 가속/감속/양쪽 |
inSine / outSine / inOutSine |
sin파. 가장 부드러운 가속/감속/양쪽 |
inExpo / outExpo / inOutExpo |
지수. 급격한 가속/감속/양쪽 |
inCirc / outCirc / inOutCirc |
원호. 끝부분에서 급격한 가속/감속/양쪽 |
inBack / outBack / inOutBack |
지나쳤다가 되돌아옴(반동) |
inElastic / outElastic / inOutElastic |
용수철 같은 진동 |
inBounce / outBounce / inOutBounce |
공이 튀는 듯한 바운드 |
중요(엔진의 실제 동작): back / elastic 계열은 도중에 진행도가 0..1 범위 밖으로 나가지만, 보간은 최근접값 클램프이므로 출력은 끝점 keyframe의 값에 머무릅니다(t=1의 값을 넘어서 튀어나오는 일은 없습니다).
"지나쳤다가 되돌아오는" 모습을 확실히 내고 싶다면, 중간 keyframe에서 명시적으로 써 주세요(예: scale을 0 → 1.15 → 1로).
4. 흔한 실수(금지 사항)
다음은 검증 오류가 되거나 의도하지 않은 모습이 됩니다. 반드시 피하세요.
- 값 범위 위반:
scale: 0이나3.5,alpha: 1.2,dx: 2.5,durationMs: 30이나5000,delayMs: 600,amplitude: 0.8,periodMs: 100등은 모두 불가. §2의 값 범위 표를 엄밀히 따를 것 - 존재하지 않는 속성: keyframe에 쓸 수 있는 것은 §2.3의 13개뿐(
t, alpha, scale, scaleX, scaleY, dx, dy, rotate, tint, strokeWidth, strokeColor, glow, glowColor).x,y,opacity,color,blur,skew,shadow등은 불가. keyframe마다의 easing 지정도 불가 - 색상을 정수 이외로 쓰기:
"#ff0066"나"red"는 불가.0xRRGGBB를 정수로 쓸 것(16진 리터럴0xff0066또는 10진16711782) - 바꾸고 싶지 않은 속성을 0으로 쓰기:
strokeWidth: 0은 "테두리를 없앤다"는 지정. 스타일의 테두리를 남기고 싶다면 쓰지 말 것 - t의 순서 위반: t의 내림차순·같은 값의 중복은 불가(진성 오름차순). 첫 항목 t=0과 마지막 항목 t=1을 잊지 말 것
- keyframes의 개수: 1개 이하·9개 이상은 불가(2~8개)
- 소수 금지 위치:
durationMs/delayMs/periodMs는 정수만 가능 - easing 이름의 표기 흔들림: §3의 31종과 엄밀히 일치하게 쓸 것
- in / out의 생략: 둘 다 필수. stagger의 3개 필드(unit / delayMs / from)도 항상 필수
- out의 t=0을 항등값 이외로 하기: 표시 중의 모습이 어긋남(§2.7)
- JSON 이외의 혼입: 주석, 끝 쉼표, 수식(
"dy": "0.5 * 2"등)은 불가. 값은 모두 숫자 리터럴이거나 허용된 문자열
5. 예시
셋 다 그대로 앱에 붙여 넣으면 통과하는 완전한 JSON입니다.
5.1 두둥실 떠오르기
글자가 조금 아래에서 부드럽게 페이드인하고, 퇴장은 위로 빠져나가며 페이드아웃.
{
"in": {
"durationMs": 700,
"easing": "outSine",
"stagger": { "unit": "char", "delayMs": 40, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 0, "dy": 0.4 },
{ "t": 1, "alpha": 1, "dy": 0 }
]
},
"out": {
"durationMs": 600,
"easing": "inSine",
"stagger": { "unit": "char", "delayMs": 30, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 1, "dy": 0 },
{ "t": 1, "alpha": 0, "dy": -0.3 }
]
}
}
5.2 글자가 회전하며 튀어 오르듯 등장
작게 회전한 상태에서, 지나침(scale 1.15)을 중간 keyframe으로 명시하여 튀어 오르는 입장. 퇴장은 끝 글자부터 역회전하며 축소.
alpha는 성기게 지정한 예: in에서는 t=0.6까지 1에 도달하고, 이후는 마지막 값 1을 유지합니다.
{
"in": {
"durationMs": 800,
"easing": "outBack",
"stagger": { "unit": "char", "delayMs": 60, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 0, "scale": 0.3, "rotate": -180 },
{ "t": 0.6, "alpha": 1, "scale": 1.15, "rotate": 20 },
{ "t": 1, "scale": 1, "rotate": 0 }
]
},
"out": {
"durationMs": 500,
"easing": "inBack",
"stagger": { "unit": "char", "delayMs": 40, "from": "end" },
"keyframes": [
{ "t": 0, "alpha": 1, "scale": 1, "rotate": 0 },
{ "t": 1, "alpha": 0, "scale": 0.3, "rotate": 180 }
]
}
}
5.3 단어마다 아래에서 슬라이드인 + 표시 중 천천히 명멸
단어 단위의 stagger로 아래에서 순서대로 나타나고, 표시 중에는 activePulse로 천천히 명멸. 퇴장은 구절 전체가 동시에 페이드아웃.
{
"in": {
"durationMs": 600,
"easing": "outCubic",
"stagger": { "unit": "word", "delayMs": 120, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 0, "dy": 0.8 },
{ "t": 1, "alpha": 1, "dy": 0 }
]
},
"out": {
"durationMs": 400,
"easing": "inQuad",
"stagger": { "unit": "none", "delayMs": 0, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 1 },
{ "t": 1, "alpha": 0 }
]
},
"activePulse": { "property": "alpha", "amplitude": 0.25, "periodMs": 1600 }
}
5.4 네온처럼 빛나며 등장
glow로 발광시키고, glowColor로 빛의 색상을 지정. 퇴장에서는 발광을 0으로 수렴시킴. tint를 쓰지 않았으므로 글자 색상은 노래에 맞춘 색 전환이 그대로 적용됩니다.
{
"in": {
"durationMs": 500,
"easing": "outCubic",
"stagger": { "unit": "char", "delayMs": 40, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 0, "glow": 0, "glowColor": 65535 },
{ "t": 0.6, "alpha": 1, "glow": 34, "glowColor": 65535 },
{ "t": 1, "alpha": 1, "glow": 20, "glowColor": 65535 }
]
},
"out": {
"durationMs": 400,
"easing": "inQuad",
"stagger": { "unit": "none", "delayMs": 0, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 1, "glow": 20, "glowColor": 65535 },
{ "t": 1, "alpha": 0, "glow": 0, "glowColor": 65535 }
]
}
}
5.5 색이 바뀌며 나타나기
tint를 움직이면 글자 색상이 보간됩니다. tint를 쓴 구절은 노래에 맞춘 색 전환을 하지 않게 되므로, 색상은 전부 이 spec에서 결정합니다.
{
"in": {
"durationMs": 700,
"easing": "outQuad",
"stagger": { "unit": "char", "delayMs": 30, "from": "center" },
"keyframes": [
{ "t": 0, "alpha": 0, "tint": 16711808 },
{ "t": 1, "alpha": 1, "tint": 16777215 }
]
},
"out": {
"durationMs": 400,
"easing": "inQuad",
"stagger": { "unit": "none", "delayMs": 0, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 1 },
{ "t": 1, "alpha": 0 }
]
}
}
5.6 찌그러졌다 늘어나며 착지(squash & stretch)
scaleX와 scaleY를 반대 방향으로 움직임. 떨어지는 동안은 세로로 길고, 착지하는 순간에 가로로 찌그러지며, 거기서부터 등배로 되돌립니다.
{
"in": {
"durationMs": 550,
"easing": "outBack",
"stagger": { "unit": "char", "delayMs": 45, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 0, "dy": -1.2, "scaleX": 0.75, "scaleY": 1.35 },
{ "t": 0.7, "alpha": 1, "dy": 0, "scaleX": 1.3, "scaleY": 0.7 },
{ "t": 1, "alpha": 1, "dy": 0, "scaleX": 1, "scaleY": 1 }
]
},
"out": {
"durationMs": 350,
"easing": "inQuad",
"stagger": { "unit": "none", "delayMs": 0, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 1 },
{ "t": 1, "alpha": 0, "scaleY": 0.6 }
]
}
}
6. AI용 프롬프트 템플릿
아래 틀 안의 내용을 복사해서 AI에게 전달하세요. 이 사양서 전체를 먼저 붙여 넣고, 그 직후에 템플릿을 이어 붙이는 방식을 권장합니다(사양서 없이 템플릿만 전달하면 AI가 값 범위나 easing 이름을 잘못 쓸 수 있습니다).
여기서부터 복사
당신은 movlyric의 가사 애니메이션 설계자입니다. 위에 붙여 넣은 "AnimationSpec v2 사양서"를 엄밀히 따라, AnimationSpec의 JSON을 하나 작성해 주세요.
출력 규칙:
- JSON만 출력한다(설명문·서두·맺음말은 쓰지 않는다. JSON을 코드 펜스로 감싸는 것은 가능)
- 루트는 {"in": ..., "out": ...}이며, 필요한 경우에만 "activePulse"를 추가한다
- keyframes는 2~8개, t는 0..1의 진성 오름차순이며, 첫 항목은 반드시 t=0, 마지막 항목은 반드시 t=1
- keyframe에 쓸 수 있는 것은 t, alpha(0..1), scale(0.1..3), scaleX(0.1..3), scaleY(0.1..3), dx(-2..2), dy(-2..2), rotate(-360..360), tint(0..16777215), strokeWidth(0..20), strokeColor(0..16777215), glow(0..40), glowColor(0..16777215)뿐이다. 단위: dx/dy는 글자 크기 비율(양수=오른쪽/아래), rotate는 도, strokeWidth/glow는 px, 색상은 0xRRGGBB의 정수
- **바꾸고 싶지 않은 속성은 쓰지 않는다.** tint를 쓰면 노래에 맞춘 색 전환이 멈추고 지정한 색으로 고정된다. strokeWidth/strokeColor를 쓰지 않으면 스타일의 테두리가 그대로 남는다(쓰지 않음 = 0이 아니다)
- 빛나게 하고 싶을 때는 glow(오프셋 없는 발광)를 사용한다. glowColor를 생략하면 글자 색상으로 빛난다
- 찌그러뜨렸다 늘리는 움직임(squash & stretch)은 scaleX와 scaleY를 반대 방향으로 움직여 만든다
- durationMs(정수 50..4000) / stagger.delayMs(정수 0..500) / activePulse.periodMs(정수 200..4000)를 지킨다. stagger는 unit("none"/"char"/"word")·delayMs·from("start"/"end"/"center") 세 가지 모두 필수
- easing은 사양서의 이징 목록에 있는 31종의 이름만 사용한다(엄밀 일치)
- out의 t=0 keyframe은 항등값(alpha 1, scale 1, dx 0, dy 0, rotate 0에 해당)으로 한다
- 존재하지 않는 속성·주석·끝 쉼표·수식을 쓰지 않는다
만들고 싶은 움직임:
{여기에 만들고 싶은 움직임을 적는다}
(기존 애니메이션을 수정하고 싶을 때만, 아래에 현재 JSON을 붙여 넣으세요. 새로 만드는 경우에는 이 절 전체를 삭제하세요)
현재 JSON:
{현재 JSON}
여기까지 복사