JSON으로 정의하는 시각화 애니메이션 패키지
명령형 코드 대신 선언적 JSON 문서로 쓰고, React · Vue · DOM · SVG 어디서든 같은 결과로 재생한다.
위 GIF는 라이브러리의 writeDocumentGif가 JSON 문서 하나로부터 직접 생성했다.
각 예시는 독립된 JSON 문서 하나이며, 아래 GIF는 모두 writeDocumentGif로 렌더링했다.
|
요소와 전이 등장 · 퇴장 모드 8종
|
이징 easing curve 4종 비교
|
|
보간 interpolation mode
|
반복 패턴repeatAppearances · stagger
|
이펙트highlight · pulse · flow
|
연결 anchor와 arrowhead
|
|
그룹 중첩 group과 변환 상속
|
챕터 캡션과 챕터 목록
|
🕒 (문서, 시각 t) → 화면 |
화면 상태는 두 값만으로 결정된다. 원하는 시점으로 이동하거나, 정지 화면을 만들거나, 서버에서 결과를 출력하거나, 에디터에서 재생 위치를 옮길 때 모두 같은 코드가 동작한다. |
| 🧊 누적 상태 없음 | 이전 상태를 누적하지 않으므로 시간이 지나면서 화면이 어긋나는 문제가 생기지 않는다. |
| 🔌 framework 비종속 scene graph | 렌더링 결과는 중립적인 scene graph다. 각 adapter는 이 결과를 React element, Vue vnode, DOM, SVG 문자열로 옮기기만 하므로 실제 렌더링 규칙은 한 곳에서 관리된다. |
| 🧩 확장은 plugin으로 | 작성 형식의 import·compile·검증·추가 export는 별도 @kokoa/clotho/plugins entry의 experimental compiler plugin API로 확장한다. runtime 의미와 모든 adapter가 공유해야 하는 근본 기능은 plugin으로 넘기지 않고 core에 내장한다. |
Tip
설치 전에 Clotho Editor에서 갤러리 애니메이션을 열고 JSON 문서와 재생 결과를 바로 확인할 수 있다. 전체 사용법과 API, JSON Schema, 표현 요소별 예시는 Clotho 문서에 있다.
npm install @kokoa/clotho
yarn add @kokoa/clotho
pnpm add @kokoa/clotho
bun add @kokoa/clothoReact와 Vue는 선택 사항이므로 사용하는 framework만 설치하면 된다. 바닐라 JavaScript와 SVG 출력에는 추가 peer dependency가 필요하지 않다.
import { parseDocumentOrThrow } from '@kokoa/clotho';
import { AnimationPlayer } from '@kokoa/clotho/react';
import '@kokoa/clotho/styles.css';
const doc = parseDocumentOrThrow(await (await fetch('/animations/bellman-ford.json')).json());
<AnimationPlayer doc={doc} />;<script setup lang="ts">
import { AnimationPlayer } from '@kokoa/clotho/vue';
import '@kokoa/clotho/styles.css';
const props = defineProps<{ doc: AnimationDocument }>();
</script>
<template><AnimationPlayer :doc="doc" /></template>import { parseDocumentOrThrow } from '@kokoa/clotho';
import { mountPlayer } from '@kokoa/clotho/dom';
import '@kokoa/clotho/styles.css';
const handle = mountPlayer(document.querySelector('#stage')!, doc);
handle.player.seek(3000);
handle.destroy();import { remarkClotho } from '@kokoa/clotho/mdx';
// remark / MDX 파이프라인에 넣는다
const plugins = [remarkClotho()];```clotho
{ "clothoVersion": 1, "id": "queue", "duration": 3000, ... }
```빌드 시 문서를 검증하고(실패하면 빌드가 실패한다) 포스터 프레임 SVG를 마크업에 인라인한다. JS 없이도 그림이 보이고, 테마 토큰이 살아 있어 다크 모드를 따라간다.
import { hydrateClothoEmbeds } from '@kokoa/clotho/mdx';
hydrateClothoEmbeds(); // 뷰포트에 들어오는 것부터 플레이어로 승격프레임워크 컴포넌트가 아니라 HTML을 내보내므로 Astro·Next(MDX)·Docusaurus·Vitepress·순수 마크다운이 모두 같은 경로를 탄다.
import { bindUrlState, shareUrl } from '@kokoa/clotho/dom';
const unbind = bindUrlState(handle.player, doc);
shareUrl(handle.player, doc); // https://…/knapsack?c=swap/animations/knapsack?c=swap&speed=1.5 처럼 특정 순간을 가리키는 링크를 만든다. 시각(t)보다 챕터(c)를 권장한다 — 문서를 고치면 t=3200은 조금씩 어긋나지만 챕터 id는 저작자가 이름 붙인 그 순간을 계속 가리킨다.
URL은 일시정지·seek·속도 변경 같은 조작에만 기록되고 재생 중에는 건드리지 않는다. replaceState를 쓰므로 뒤로 가기가 애니메이션 히스토리로 채워지지 않는다.
<script type="module">
import { defineClothoPlayer } from '@kokoa/clotho/element';
defineClothoPlayer();
</script>
<clotho-player src="/animations/knapsack.json" theme="dark" autoplay loop></clotho-player>문서를 인라인으로 넣으면 네트워크 요청도 사라진다.
<clotho-player>
<script type="application/json">{ "clothoVersion": 1, ... }</script>
</clotho-player>shadow root에 렌더하고 스타일시트를 함께 들고 간다 — 남의 페이지에 얹힐 때 호스트 CSS와 충돌하지 않는다. 테마 토큰(--cloth-*)은 CSS 변수라 경계를 통과하므로 호스트의 팔레트 오버라이드는 그대로 동작한다.
el.player로 seek·play·setSpeed를 부를 수 있고, clotho-ready · clotho-chapterchange · clotho-ended · clotho-error 이벤트가 올라온다.
import { mountPresenter } from '@kokoa/clotho/dom';
mountPresenter(document.body, doc);→/Space 다음 구간, ← 이전, P 재생/정지, N 발표자 노트, B 블랙아웃, F 전체화면.
챕터를 구간으로 다루므로 다음을 누르면 그 구간을 재생하고 멈춘다. 챕터 시각으로 점프하지 않는다 — 애니메이션이 설명의 일부라서 건너뛰면 청중은 과정 없는 결과만 본다. 같은 문서가 블로그 글이자 강의 자료가 되므로 내용이 갈라지지 않는다.
import { mountScrollPlayer } from '@kokoa/clotho/dom';
mountScrollPlayer(document.querySelector('#stage')!, doc, {
pin: true, // 구간 동안 stage를 sticky로 고정
snapToChapters: true, // 챕터마다 같은 양의 스크롤을 배분
});시계를 스크롤로 갈아끼운다. Player가 이미 프레임워크 밖에 있고 seek(t)가 순수하기 때문에 되감기가 자유롭다 — 위로 스크롤하는 것은 더 작은 숫자이지 되돌리기가 아니다.
prefers-reduced-motion에서는 스크롤 연동을 끄고 챕터별 정지 프레임 목록으로 강등한다. 스크롤 하이재킹은 전정기관 문제가 있는 독자에게 특히 나쁘고, 속도를 늦추는 것으로는 해결되지 않는다.
import { renderDocumentToSvg } from '@kokoa/clotho/svg';
const svg = renderDocumentToSvg(doc, 6000, { standalone: true });DOM도 프레임워크도 필요 없다. 프레임 단위로 호출하면 그대로 정지 프레임 시퀀스가 된다.
clotho storyboard knapsack.json --out sheet.png --per-row 3
clotho storyboard knapsack.json --out frames/ # 프레임마다 SVG 한 장
clotho storyboard knapsack.json --out sheet.png --frames count --count 6인쇄물·논문 PDF·발표 슬라이드·코드 리뷰 코멘트처럼 애니메이션이 재생될 수 없는 자리를 위한 정지 이미지 묶음이다. 지금까지 출력은 SVG 한 프레임 아니면 GIF 전체였는데, 전자는 과정을 못 담고 후자는 붙일 수 없는 곳이 많다.
프레임은 기본적으로 챕터에서 고른다 — 저작자가 이미 단계를 나눠 놓았으므로 어떤 휴리스틱보다 낫다. 챕터가 없으면 고르게 퍼뜨린다.
import { writeDocumentGif } from '@kokoa/clotho/gif';
await writeDocumentGif(doc, 'knapsack.gif', {
fps: 12,
width: 800,
background: '#ffffff',
layout: 'player', // 제목, 조작 버튼, 현재 단계 설명, 전체 단계 목록까지 포함한다. 기본값이다.
});애니메이션 영역만 필요하면 layout: 'stage'를 지정한다. 기본 renderer는 운영체제의 글꼴을 불러와 문자를 빠짐없이 그린다. CI처럼 실행 환경이 달라도 같은 결과가 필요하다면 fontFiles: ['/path/font.ttf']로 사용할 글꼴을 직접 지정할 수 있다. CSS 색상 token은 theme: 'light' | 'dark'에서 선택한 테마의 실제 색상으로 변환한다.
CLI에서도 같은 렌더 경로를 쓴다.
clotho gif animations/knapsack.json knapsack.gif --fps 12 --width 800작성 도우미는 별도의 실행 형식을 만들지 않는다. type을 검사하고 반복 표현을 실제 항목으로 펼친 뒤 에디터와 CLI가 그대로 읽을 수 있는 v1 JSON data를 반환한다.
import { appear, defineAnimation, effects, repeatAppearances, stagger, track } from '@kokoa/clotho';
const doc = defineAnimation({
clothoVersion: 1,
id: 'queue',
duration: 3000,
elements: [
{
type: 'circle',
id: 'item',
cx: 40,
cy: 80,
r: 16,
appearances: repeatAppearances({ count: 3, duration: 600, gap: 200 }),
tracks: [
track('cx', [
{ time: 0, value: 40 },
{ time: 3000, value: 600, ease: 'easeOut' },
]),
],
},
],
effects: stagger(['item'], 150, (elementId, time, index) =>
effects.pulse({ id: `pulse-${index}`, elementId, time }),
),
});| 진입점 | 내용 | peer | gzip |
|---|---|---|---|
@kokoa/clotho |
스키마·런타임·씬 그래프·재생 컨트롤러·검증·마이그레이션 | 없음 | 25KB |
…/svg |
SVG 문자열 직렬화 | 없음 | 16KB |
…/dom |
바닐라 JS 어댑터 + 브라우저 스케줄러 | 없음 | 20KB |
…/react |
React 어댑터 + 컴포넌트·훅 | react, react-dom | 20KB |
…/vue |
Vue 3 어댑터 + 컴포넌트 | vue | 20KB |
…/node |
파일시스템 문서 로더 | 없음 | 별도 진입점 |
…/gif |
Node/Bun용 애니메이션 GIF 렌더러 | 없음 | 별도 진입점 |
…/mdx |
마크다운 embed 빌더 + hydration | 없음 | 별도 진입점 |
…/element |
<clotho-player> 커스텀 엘리먼트 |
없음 | 별도 진입점 |
…/styles.css |
스타일시트 | 없음 | 4KB |
…/schema.json |
v1 JSON Schema (에디터 자동완성용) | — | — |
Note
렌더 어댑터는 이미 파싱된 문서를 받으므로 zod를 포함하지 않는다. 렌더만 하는 소비처는 검증기 비용을 내지 않고, 이 성질은 bun run check:size로 강제된다.
clotho validate animations/ # 스키마 + 의미 검증
clotho validate animations/ --strict # 경고도 실패로
clotho migrate animations/ --write # legacy v3/v4 → v1 변환
clotho gif animations/a.json a.gif --fps 12 --width 800
clotho explain doc.json --at 3200 # 그 시점 화면이 왜 그런지
clotho diff before.json after.json # 무엇이 바뀌었는지 사람 말로
clotho diff a.json b.json --format md # PR 코멘트에 붙일 표
clotho sync animations/ # source 연결된 code 요소를 파일에서 갱신
clotho sync animations/ --check # 쓰지 않고, 낡았으면 종료 코드 1
clotho dev animations/ # 라이브 리로드 프리뷰 (127.0.0.1 전용)
clotho dev animations/ --headless # 페이지 없이 감시 + 검증clotho dev는 디렉터리의 문서를 목록으로 띄우고, 파일이 밖에서 바뀌면 재생 위치를 유지한 채 다시 그리며, 검증과 린트 결과를 같은 화면에 보여준다. 브라우저는 임의의 로컬 디렉터리를 열 수 없으므로 이 루프는 에디터가 채울 수 없는 자리다.
검증기는 스키마가 잡지 못하는 것들을 본다: 중복 id, 참조 무결성, 시간 범위, parentId 순환, 미해결 에셋, 그리고 스키마에 없는 속성. 마지막 항목이 특히 쓸모 있다 — 파서는 미지의 키를 조용히 버리므로, 작성자가 line.label(라벨은 arrow에만 있다)이나 arrow.arrowEnd(필드명은 headEnd)를 써도 아무 일도 일어나지 않는다. 이 패키지를 추출한 383개 실문서에는 그런 속성이 367개 있었다.
// 파싱 — 실패 시 예외 대신 이슈 목록을 반환값에 담는다
const result = parseDocument(json);
if (!result.ok) console.error(result.issues);
// 한 시점의 시각 상태 (순수)
const snapshot = computeSnapshot(doc, 3000);
// 한 시점의 렌더 데이터 (순수, 프레임워크 무관)
const scene = buildScene(doc, 3000, { assetResolver, highlighter });
// 재생 — 프레임워크 밖에 있다
const player = createPlayer(doc, { scheduler: animationFrameScheduler });
player.subscribe((state) => console.log(state.time, state.chapterIndex));
player.play();
player.seek(5000);
player.setSpeed(1.5);
player.destroy();
// 검증 / 마이그레이션
const { ok, findings } = validateDocument(json);
const { document, notes } = migrateLegacyDocument(legacyJson);호스트 훅 — 패키지가 결정하지 않고 소비처가 주입하는 것들
buildScene(doc, t, {
assetResolver: { resolve: (ref) => myCdn.urlFor(ref.key) }, // ref 에셋 해석
highlighter: myShikiHighlighter, // 코드 하이라이팅
measurer: { measure: (text, size) => canvasMeasure(text, size) }, // 실측 폰트 메트릭
fontFamily: '"My Sans", sans-serif',
});이미지는 문서에 base64로 담거나(inline), URL로 두거나(external), 호스트가 해석하는 키로 둘 수 있다(ref). 에디터의 "이미지 첨부"는 encodeImageAsset(bytes, mime)으로 만든다.
테마 — 라이트·다크 토큰과 팔레트 오버라이드
스타일시트는 --cloth-* 토큰에 라이트·다크 기본값을 모두 담고 있어 설정 없이 동작한다. 플레이어별로 theme="light" | "dark" | "auto"를 지정할 수 있고, DOM 어댑터도 같은 theme 옵션을 받는다.
<AnimationPlayer doc={doc} theme="dark" />자기 팔레트를 쓰려면 테마별 토큰을 매핑한다:
[data-cloth-theme='light'] {
--cloth-fg: var(--light-fg);
--cloth-surface: var(--light-surface);
--cloth-accent: var(--light-brand);
}
[data-cloth-theme='dark'] {
--cloth-fg: var(--dark-fg);
--cloth-surface: var(--dark-surface);
--cloth-accent: var(--dark-brand);
}
/* auto 모드의 기본 토큰도 필요하면 :root에서 바꾼다. */
:root {
--cloth-fg: var(--color-fg);
--cloth-accent: var(--brand-500);
}테마는 기본적으로 prefers-color-scheme을 따르고, 어느 조상에든 data-cloth-theme을 두면 그것이 이긴다.
챕터 목록 위치 — 좌·우·상·하 배치
showChapterList가 켜져 있고 실제 챕터가 있을 때만 목록이 보인다. 위치는 문서에서 좌·우·상·하로 지정하며 기본값은 기존 사이드바와 같은 right다.
{
"settings": {
"showCaption": true,
"showChapterList": true,
"chapterListPosition": "right"
}
}애니메이션을 직접 쓰려면 docs/AUTHORING.md부터 읽는다. 필드별 정의는 docs/SCHEMA-V1.md에 있다.
요소 11종 (rect · circle · line · arrow · text · image · path · polygon · group · code · math), 등장 구간(appearances)과 속성 트랙(tracks)으로 이루어진 타임라인, 이펙트 5종 (highlight · pulse · flow · spotlight · trail), 챕터, 카메라.
Important
legacy v3/v4 문서는 런타임이 직접 받지 않는다. clotho migrate를 통과해야 한다.
기존 content는 항상 기본 문구로 사용되므로 이전 문서도 그대로 동작한다. 문서의 locales를 생략하면 ko, en을 기본으로 제공하며, 특정 text 요소에서 다른 언어가 필요하면 locales와 translations를 직접 확장한다.
{
"locales": ["ko", "en", "ja", "zh-CN"],
"elements": [
{
"type": "text",
"id": "welcome",
"x": 400,
"y": 240,
"content": "환영합니다",
"translations": {
"en": "Welcome",
"ja": "ようこそ",
"zh-CN": "欢迎"
}
}
]
}host는 현재 언어를 SceneOptions.locale로 전달한다. 등록한 번역이 없거나 locale을 전달하지 않으면 content로 돌아간다.
<AnimationPlayer doc={doc} options={{ locale: currentLocale }} />UI 문자열 기본값은 영어이며 부분 오버라이드가 가능하다. 한국어 문구는 koreanStrings로 제공한다.
<AnimationPlayer doc={doc} strings={{ play: '재생', pause: '일시정지' }} />bun run gallery # 기능별 문서 9개 — 요소 10종, 전이 8종, 이징 4종, 반복 패턴각 문서에 "무엇을 볼 것인가"가 붙어 있고, frames 버튼이 타임라인 전체를 한 번에 펼친다. examples/README.md 참고.
| 문서 | 내용 |
|---|---|
docs/AUTHORING.md |
애니메이션 저작 공식 문서 |
docs/SCHEMA-V1.md |
v1 문서 포맷 명세 |
docs/ARCHITECTURE.md |
씬 그래프, 어댑터, 재생 컨트롤러 |
docs/PLUGINS.md |
experimental compiler plugin API |
docs/RESEARCH.md |
기존 두 구현체 실측 조사 |
docs/MIGRATION.md |
legacy 문서·코드 이전 |
docs/AUDIT-EDITOR.md |
에디터 기능 커버리지 감사 |
docs/PROPOSALS.md |
확장 기획안 14건 |
docs/RELEASING.md |
배포 전 검증·npm 업로드·설치 확인 |
TASKS.md |
작업 계획과 진행 상황 |
에디터는 별도 패키지 clotho-editor로 분리된다.
bun install
bun test # 유닛 + 코퍼스 회귀 + 어댑터 동등성
bun run typecheck
bun run lint
bun run build
bun run check:core-purity # 코어에 프레임워크·DOM 의존 유입 차단
bun run check:styles # 클래스·토큰·테마 경로 일치
bun run check:size # 진입점별 크기 예산 + 서브패스 격리
bun run schema:check # JSON Schema가 zod와 동기인지
bun run release:check # 패키지 메타데이터와 tarball 내용 검사
bash scripts/verify-package-managers.sh # npm/yarn/bun 로컬 설치 검사.private/에 참조 저장소가 있을 때만 도는 검사
bun run check:legacy-equivalence # legacy 엔진과 렌더 동등성 (383개 × 27,690 프레임)
bun run check:svg-wellformed # 1,915 프레임을 실제 XML 파서로 검증회귀 테스트는 실제 애니메이션 문서 383개를 픽스처로 쓴다. 저장소에 포함되지 않으므로 없으면 자동으로 건너뛴다. 위치는 CLOTHO_CORPUS_DIR로 지정한다.
기여 방법은 CONTRIBUTING.md, 변경 이력은 CHANGELOG.md에 있다.
MIT © kokoa








{ "clothoVersion": 1, "id": "bellman-ford", "title": "벨만-포드", "duration": 12000, "canvas": { "width": 800, "height": 460, "background": "transparent" }, "elements": [ { "type": "circle", "id": "n-a", "cx": 150, "cy": 230, "r": 32, "fill": "#fef3c7", "label": "A", "appearances": [{ "start": 0, "end": 12000 }], "tracks": [ { "property": "fill", "keyframes": [ { "time": 0, "value": "#fef3c7" }, { "time": 2000, "value": "#dcfce7" }, ], }, ], }, ], "chapters": [{ "id": "c1", "time": 2000, "label": "Round 1" }], "effects": [{ "type": "pulse", "id": "p1", "elementId": "n-a", "time": 2000 }], }