AnimationSpec v2 사양서 — 나만의 "문자 움직임" 만들기

이 문서는 일본어 원문의 번역본입니다. 일본어판이 정본이며, 내용에 차이가 있을 경우 일본어판이 우선합니다.

이 문서는 movlyric의 문자 움직임(가사 애니메이션) 정의인 "AnimationSpec v2"의 완전한 사양입니다. 앱의 "스타일 에디터 → 「움직임」 탭 → 문자 움직임 → 「사용자 지정」 → AI로 만들기…"에서 사용합니다(배경에 도형을 그리는 "배경 이펙트"는 별개이며, 그쪽은 GRAPHIC_SPEC(배경 이펙트)가 사양입니다). v1(모션만 있던 버전)으로 작성된 JSON은 그대로 v2로도 유효합니다. v2에서는 색상·테두리·발광·축별 신축이 추가되었습니다. 이 문서만 읽은 AI(또는 사람)가 앱에 그대로 붙여 넣어 통과하는 JSON을 작성할 수 있게 하는 것이 목적입니다. 문서 끝에 AI에게 전달할 프롬프트 템플릿이 있습니다. 이 문서 전체를 프롬프트의 일부로 붙여 넣어도 작동합니다.

1. 개요

movlyric은 가사(구절)를 시간에 맞춰 화면에 표시하는 동영상 엔진입니다. 하나의 구절은 다음 3단계로 움직입니다.

  1. 입장(in): 구절 표시 창의 시작(구절 발성 시작 약 1초 전)부터, 문자/단어가 나타나는 움직임
  2. 표시 중(정지 상태): 입장이 끝난 상태로 정지. 필요하면 activePulse(sin파 맥동)를 걸 수 있음
  3. 퇴장(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)입니다. 이것이 표시 창보다 길면 타임라인 전체가 등배로 압축됩니다(창이 짧아도 글자가 나타나지 않은 채로 남지 않도록). 글자 수가 많은 가사에서 char stagger에 큰 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}

여기까지 복사