@kokoa/clotho-editor 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -26
- package/dist/{chunk-UE32Q6U5.js → chunk-GMGB7NIL.js} +198 -103
- package/dist/chunk-GMGB7NIL.js.map +1 -0
- package/dist/clotho-editor.css +150 -18
- package/dist/index.d.ts +40 -2
- package/dist/index.js +129 -17
- package/dist/index.js.map +1 -1
- package/dist/{main-SWHHYTLM.js → main-NCPWDB2U.js} +2417 -1603
- package/dist/main-NCPWDB2U.js.map +1 -0
- package/docs/PORTING.md +16 -33
- package/package.json +10 -4
- package/dist/chunk-UE32Q6U5.js.map +0 -1
- package/dist/main-SWHHYTLM.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,53 +1,106 @@
|
|
|
1
1
|
# @kokoa/clotho-editor
|
|
2
2
|
|
|
3
|
-
[
|
|
3
|
+
`@kokoa/clotho-editor`는 [Clotho](https://github.com/shinkeonkim/clotho) 애니메이션 문서를 만드는 React 기반 시각 편집기입니다. 저장소, 이미지 업로드, 예제 목록과 저장 동작을 host application에서 주입할 수 있으므로 독립형 편집기와 기존 관리 화면에서 같은 package를 사용할 수 있습니다.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
실제 화면은 [Clotho Editor](https://clotho-editor.shinkeonkim.com/)에서 확인할 수 있습니다.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
기반 독립 패키지로 옮겼다. typecheck 0 errors, 빌드·테스트 통과.
|
|
9
|
-
|
|
10
|
-
이식에서 가장 의미 있는 변화는 **미리보기가 clotho를 쓴다**는 점이다. Studio는 자체
|
|
11
|
-
캔버스 렌더를 갖고 있어 에디터에서 맞게 보이는 것이 사이트에서 다르게 보일 수 있었다.
|
|
12
|
-
이제 `buildScene` + `patchScene`을 지나므로 에디터와 배포본이 갈라질 수 없다.
|
|
13
|
-
|
|
14
|
-
자세한 내용과 남은 정리 항목은 [`docs/PORTING.md`](./docs/PORTING.md).
|
|
15
|
-
|
|
16
|
-
## 사용
|
|
7
|
+
## 설치
|
|
17
8
|
|
|
18
9
|
```bash
|
|
19
10
|
npm install @kokoa/clotho @kokoa/clotho-editor react react-dom
|
|
20
|
-
# yarn add / pnpm add / bun add 도 같은 패키지 목록을 사용한다.
|
|
21
11
|
```
|
|
22
12
|
|
|
23
13
|
```tsx
|
|
24
|
-
import {
|
|
14
|
+
import { StudioMount, createLocalStorageRepository } from "@kokoa/clotho-editor";
|
|
15
|
+
import "@kokoa/clotho/styles.css";
|
|
25
16
|
import "@kokoa/clotho-editor/styles.css";
|
|
26
17
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
const repository = createLocalStorageRepository({
|
|
19
|
+
storageKey: "my-app.animations",
|
|
20
|
+
examples,
|
|
21
|
+
});
|
|
30
22
|
|
|
31
|
-
|
|
23
|
+
export function AnimationEditor() {
|
|
24
|
+
return <StudioMount editorTitle="Clotho Editor" repository={repository} />;
|
|
25
|
+
}
|
|
32
26
|
```
|
|
33
27
|
|
|
34
|
-
`
|
|
35
|
-
완성형 화면이 필요하면 `<StudioMount initialId="document-id" />`를 사용한다. 일반적인
|
|
36
|
-
Vite/Next.js/React SPA에 둘 다 삽입할 수 있고, 브라우저 전용 코드는 mount 이후에
|
|
37
|
-
초기화된다.
|
|
28
|
+
`StudioMount`는 목록 조회, 새 문서 생성, 저장, 삭제와 JSON 내보내기를 포함한 편집기 화면입니다. 편집기 shell을 application이 직접 구성해야 한다면 `Studio`를 사용할 수 있습니다.
|
|
38
29
|
|
|
39
|
-
##
|
|
30
|
+
## 저장소 연결
|
|
31
|
+
|
|
32
|
+
편집기는 특정 backend에 의존하지 않습니다. `AnimationRepository`를 구현하면 REST API, browser storage, Git 기반 저장소 등 원하는 데이터 계층을 연결할 수 있습니다.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import type { AnimationRepository } from "@kokoa/clotho-editor";
|
|
36
|
+
|
|
37
|
+
const repository: AnimationRepository = {
|
|
38
|
+
list: async () => api.list(),
|
|
39
|
+
load: async (id) => api.load(id),
|
|
40
|
+
create: async (id, title) => api.create(id, title),
|
|
41
|
+
save: async (document) => api.save(document),
|
|
42
|
+
delete: async (id) => api.delete(id),
|
|
43
|
+
};
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
간단한 demo나 offline 편집기는 `createLocalStorageRepository`를 사용하면 됩니다. 기존 HTTP API를 사용하는 application은 `configureApi({ baseUrl })`로 기본 adapter를 설정할 수도 있습니다.
|
|
47
|
+
|
|
48
|
+
## Host hooks
|
|
49
|
+
|
|
50
|
+
상단 제목과 이미지 처리 방식은 실행 환경에서 정할 수 있습니다. `resolveImage`를 지정하지 않으면 이미지를 data URL로 변환해 문서의 `assets`에 저장합니다. 같은 이미지는 하나의 asset을 공유하며, 사용되지 않는 asset은 JSON을 내보낼 때 제거됩니다.
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<StudioMount
|
|
54
|
+
editorTitle="블로그 애니메이션 편집기"
|
|
55
|
+
repository={repository}
|
|
56
|
+
resolveImage={async (file) => uploadToMediaServer(file)}
|
|
57
|
+
/>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
저장 버튼의 동작 자체를 바꾸려면 repository의 `create`와 `save`를 구현합니다. 예제 목록은 repository의 `list`와 `load`에서 제공하므로 editor package 안에 application별 API 경로를 넣을 필요가 없습니다.
|
|
61
|
+
|
|
62
|
+
## 주요 기능
|
|
63
|
+
|
|
64
|
+
- Clotho v1 JSON 문서 작성과 검증
|
|
65
|
+
- 사각형, 원, 선, 화살표, 텍스트, 이미지, Path, 다각형과 그룹 편집
|
|
66
|
+
- anchor를 유지하는 선 연결과 다중 선택
|
|
67
|
+
- keyframe, effect, chapter와 재생 설정 편집
|
|
68
|
+
- light/dark theme와 단계 목록 위치 설정
|
|
69
|
+
- 별도 브라우저 창으로 분리할 수 있는 timeline
|
|
70
|
+
- 실제 `@kokoa/clotho/dom` player를 사용하는 독립 미리보기
|
|
71
|
+
- JSON 가져오기와 내보내기, 참조되지 않는 image asset 정리
|
|
72
|
+
|
|
73
|
+
도구 단축키는 편집기 안에서 확인할 수 있으며 input, textarea와 contenteditable에 입력하는 동안에는 동작하지 않습니다.
|
|
74
|
+
|
|
75
|
+
## 독립 실행형 앱
|
|
76
|
+
|
|
77
|
+
저장소의 `app`은 package 통합 예제이자 Cloudflare Workers Static Assets용 demo입니다.
|
|
40
78
|
|
|
41
79
|
```bash
|
|
42
80
|
bun install
|
|
81
|
+
bun run app:dev
|
|
82
|
+
bun run build:app
|
|
83
|
+
bun run dev:cloudflare
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Cloudflare Dashboard에서는 Build command를 `bun run build:app`, Deploy command를 `npx wrangler deploy`로 지정합니다. custom domain은 Dashboard에서 별도로 연결합니다.
|
|
87
|
+
|
|
88
|
+
## 개발
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
bun install --frozen-lockfile
|
|
43
92
|
bun run typecheck
|
|
44
93
|
bun test
|
|
45
94
|
bun run build
|
|
46
95
|
```
|
|
47
96
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
97
|
+
npm release는 GitHub Release와 [publish workflow](./.github/workflows/publish.yml)를 통해 진행합니다. `@kokoa/clotho`의 호환 버전이 먼저 공개되어 있어야 합니다.
|
|
98
|
+
|
|
99
|
+
## 문서
|
|
100
|
+
|
|
101
|
+
- [Clotho 저장소](https://github.com/shinkeonkim/clotho)
|
|
102
|
+
- [Clotho Editor demo](https://clotho-editor.shinkeonkim.com/)
|
|
103
|
+
- [Porting 기록](./docs/PORTING.md)
|
|
51
104
|
|
|
52
105
|
## 라이선스
|
|
53
106
|
|
|
@@ -1,6 +1,163 @@
|
|
|
1
|
-
import { computeSnapshot, encodeImageAsset, inlineAssetFromDataUri
|
|
1
|
+
import { animationDocumentSchema, computeSnapshot, encodeImageAsset, inlineAssetFromDataUri } from '@kokoa/clotho';
|
|
2
2
|
|
|
3
|
-
// src/
|
|
3
|
+
// src/export-json.ts
|
|
4
|
+
function animationDocumentToJson(def) {
|
|
5
|
+
const parsed = garbageCollectAnimationAssets(def);
|
|
6
|
+
return `${JSON.stringify(parsed, null, 2)}
|
|
7
|
+
`;
|
|
8
|
+
}
|
|
9
|
+
function garbageCollectAnimationAssets(def) {
|
|
10
|
+
const parsed = animationDocumentSchema.parse(def);
|
|
11
|
+
const used = new Set(
|
|
12
|
+
parsed.elements.filter((element) => element.type === "image").map((element) => element.assetId)
|
|
13
|
+
);
|
|
14
|
+
return animationDocumentSchema.parse({
|
|
15
|
+
...parsed,
|
|
16
|
+
assets: Object.fromEntries(
|
|
17
|
+
Object.entries(parsed.assets).filter(([id]) => used.has(id))
|
|
18
|
+
)
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function animationDocumentFileName(def) {
|
|
22
|
+
const id = def.id.trim().replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
23
|
+
return `${id || "animation"}.json`;
|
|
24
|
+
}
|
|
25
|
+
function downloadAnimationJson(def) {
|
|
26
|
+
if (typeof document === "undefined" || typeof URL.createObjectURL !== "function") {
|
|
27
|
+
throw new Error("JSON \uD30C\uC77C\uC740 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C\uB9CC \uB0B4\uB824\uBC1B\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
|
|
28
|
+
}
|
|
29
|
+
const url = URL.createObjectURL(
|
|
30
|
+
new Blob([animationDocumentToJson(def)], {
|
|
31
|
+
type: "application/json;charset=utf-8"
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
const link = document.createElement("a");
|
|
35
|
+
link.href = url;
|
|
36
|
+
link.download = animationDocumentFileName(def);
|
|
37
|
+
link.hidden = true;
|
|
38
|
+
document.body.append(link);
|
|
39
|
+
link.click();
|
|
40
|
+
link.remove();
|
|
41
|
+
URL.revokeObjectURL(url);
|
|
42
|
+
}
|
|
43
|
+
var DEFAULT_BASE = "/api/admin/animations";
|
|
44
|
+
var BASE = DEFAULT_BASE;
|
|
45
|
+
function configureApi(options) {
|
|
46
|
+
if (options.baseUrl) BASE = options.baseUrl.replace(/\/+$/, "");
|
|
47
|
+
repository = httpRepository;
|
|
48
|
+
}
|
|
49
|
+
function apiBaseUrl() {
|
|
50
|
+
return BASE;
|
|
51
|
+
}
|
|
52
|
+
var revisions = /* @__PURE__ */ new Map();
|
|
53
|
+
var MissingAnimationRevisionError = class extends Error {
|
|
54
|
+
};
|
|
55
|
+
var AnimationStudioApiError = class extends Error {
|
|
56
|
+
};
|
|
57
|
+
function isRecord(value) {
|
|
58
|
+
return typeof value === "object" && value !== null;
|
|
59
|
+
}
|
|
60
|
+
async function readJson(res) {
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const text = await res.text();
|
|
63
|
+
throw new AnimationStudioApiError(`HTTP ${res.status}: ${text}`);
|
|
64
|
+
}
|
|
65
|
+
return res.json();
|
|
66
|
+
}
|
|
67
|
+
function parseRevision(value) {
|
|
68
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
|
|
69
|
+
throw new TypeError("Animation revision is malformed");
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
function parseAnimationEnvelope(value) {
|
|
74
|
+
if (!isRecord(value)) throw new TypeError("Animation response is malformed");
|
|
75
|
+
return {
|
|
76
|
+
def: animationDocumentSchema.parse(value.def),
|
|
77
|
+
revision: parseRevision(value.revision)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
var httpRepository = {
|
|
81
|
+
list: async () => {
|
|
82
|
+
const res = await fetch(BASE);
|
|
83
|
+
const data = await readJson(res);
|
|
84
|
+
if (!isRecord(data) || !Array.isArray(data.items))
|
|
85
|
+
throw new TypeError("Animation list response is malformed");
|
|
86
|
+
return data.items.map((item) => {
|
|
87
|
+
if (!isRecord(item) || typeof item.id !== "string" || typeof item.title !== "string" || typeof item.description !== "string") {
|
|
88
|
+
throw new TypeError("Animation summary is malformed");
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
id: item.id,
|
|
92
|
+
title: item.title,
|
|
93
|
+
description: item.description,
|
|
94
|
+
...typeof item.updatedAt === "string" ? { updatedAt: item.updatedAt } : {}
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
load: async (id) => {
|
|
99
|
+
const res = await fetch(`${BASE}/${encodeURIComponent(id)}`);
|
|
100
|
+
const data = parseAnimationEnvelope(await readJson(res));
|
|
101
|
+
revisions.set(data.def.id, data.revision);
|
|
102
|
+
return data.def;
|
|
103
|
+
},
|
|
104
|
+
save: async (def) => {
|
|
105
|
+
const revision = revisions.get(def.id);
|
|
106
|
+
if (revision === void 0)
|
|
107
|
+
throw new MissingAnimationRevisionError(
|
|
108
|
+
`Animation '${def.id}' has no loaded revision`
|
|
109
|
+
);
|
|
110
|
+
const res = await fetch(`${BASE}/${encodeURIComponent(def.id)}`, {
|
|
111
|
+
method: "PUT",
|
|
112
|
+
headers: { "Content-Type": "application/json" },
|
|
113
|
+
body: JSON.stringify({ def, revision })
|
|
114
|
+
});
|
|
115
|
+
const data = parseAnimationEnvelope(await readJson(res));
|
|
116
|
+
revisions.set(data.def.id, data.revision);
|
|
117
|
+
return data.def;
|
|
118
|
+
},
|
|
119
|
+
create: async (id, title) => {
|
|
120
|
+
const res = await fetch(BASE, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: { "Content-Type": "application/json" },
|
|
123
|
+
body: JSON.stringify({ id, title })
|
|
124
|
+
});
|
|
125
|
+
const data = parseAnimationEnvelope(await readJson(res));
|
|
126
|
+
revisions.set(data.def.id, data.revision);
|
|
127
|
+
return data.def;
|
|
128
|
+
},
|
|
129
|
+
delete: async (id) => {
|
|
130
|
+
const res = await fetch(`${BASE}/${encodeURIComponent(id)}`, {
|
|
131
|
+
method: "DELETE"
|
|
132
|
+
});
|
|
133
|
+
await readJson(res);
|
|
134
|
+
revisions.delete(id);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
var repository = httpRepository;
|
|
138
|
+
function configureAnimationRepository(next) {
|
|
139
|
+
repository = next;
|
|
140
|
+
}
|
|
141
|
+
var listAnimations = () => repository.list();
|
|
142
|
+
var loadAnimation = (id) => repository.load(id);
|
|
143
|
+
var saveAnimation = (def) => repository.save(def);
|
|
144
|
+
var createAnimation = (id, title) => repository.create(id, title);
|
|
145
|
+
var deleteAnimation = (id) => repository.delete(id);
|
|
146
|
+
async function importAnimation(def) {
|
|
147
|
+
const parsed = animationDocumentSchema.parse(def);
|
|
148
|
+
try {
|
|
149
|
+
await repository.load(parsed.id);
|
|
150
|
+
} catch {
|
|
151
|
+
await repository.create(parsed.id, parsed.title || parsed.id);
|
|
152
|
+
}
|
|
153
|
+
return repository.save(parsed);
|
|
154
|
+
}
|
|
155
|
+
async function duplicateAnimation(sourceId, newId, newTitle) {
|
|
156
|
+
const source = await loadAnimation(sourceId);
|
|
157
|
+
const cloned = { ...source, id: newId, title: newTitle };
|
|
158
|
+
await createAnimation(newId, newTitle);
|
|
159
|
+
return await saveAnimation(cloned);
|
|
160
|
+
}
|
|
4
161
|
var listeners = /* @__PURE__ */ new Set();
|
|
5
162
|
var state = {
|
|
6
163
|
def: null,
|
|
@@ -44,7 +201,19 @@ function mutateDef(fn, label = "edit", kind = "other") {
|
|
|
44
201
|
console.warn("[studio.state] invalid mutation", parsed.error.issues);
|
|
45
202
|
return;
|
|
46
203
|
}
|
|
47
|
-
|
|
204
|
+
const localized = cloned;
|
|
205
|
+
const next = parsed.data;
|
|
206
|
+
if (localized.locales) next.locales = localized.locales;
|
|
207
|
+
next.elements = next.elements.map((element, index) => {
|
|
208
|
+
const source = localized.elements[index];
|
|
209
|
+
if (source?.id !== element.id || element.type !== "text") return element;
|
|
210
|
+
return {
|
|
211
|
+
...element,
|
|
212
|
+
...source.locales ? { locales: source.locales } : {},
|
|
213
|
+
...source.translations ? { translations: source.translations } : {}
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
state.def = next;
|
|
48
217
|
state.dirty = true;
|
|
49
218
|
emit();
|
|
50
219
|
}
|
|
@@ -548,6 +717,15 @@ function updateMeta(patch) {
|
|
|
548
717
|
"meta"
|
|
549
718
|
);
|
|
550
719
|
}
|
|
720
|
+
function updateLocales(locales) {
|
|
721
|
+
mutateDef(
|
|
722
|
+
(def) => {
|
|
723
|
+
def.locales = locales;
|
|
724
|
+
},
|
|
725
|
+
"\uBB38\uC11C \uC5B8\uC5B4 \uBCC0\uACBD",
|
|
726
|
+
"meta"
|
|
727
|
+
);
|
|
728
|
+
}
|
|
551
729
|
function updateCanvas(patch) {
|
|
552
730
|
const keys = Object.keys(patch).join(", ");
|
|
553
731
|
mutateDef(
|
|
@@ -608,6 +786,13 @@ function registerExternalAsset(url) {
|
|
|
608
786
|
}
|
|
609
787
|
function registerInlineAsset(bytes, mime) {
|
|
610
788
|
const { asset } = encodeImageAsset(bytes, mime);
|
|
789
|
+
const def = getDef();
|
|
790
|
+
if (def) {
|
|
791
|
+
for (const [id2, existing] of Object.entries(def.assets)) {
|
|
792
|
+
if (existing.kind === "inline" && existing.mime === asset.mime && existing.data === asset.data)
|
|
793
|
+
return id2;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
611
796
|
const id = uniqueAssetId();
|
|
612
797
|
mutateDef(
|
|
613
798
|
(draft) => {
|
|
@@ -628,6 +813,13 @@ function uniqueAssetId() {
|
|
|
628
813
|
function registerDataUriAsset(dataUri) {
|
|
629
814
|
const inline = inlineAssetFromDataUri(dataUri);
|
|
630
815
|
if (!inline) return registerExternalAsset(dataUri);
|
|
816
|
+
const def = getDef();
|
|
817
|
+
if (def) {
|
|
818
|
+
for (const [id2, existing] of Object.entries(def.assets)) {
|
|
819
|
+
if (existing.kind === "inline" && existing.mime === inline.mime && existing.data === inline.data)
|
|
820
|
+
return id2;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
631
823
|
const id = uniqueAssetId();
|
|
632
824
|
mutateDef(
|
|
633
825
|
(draft) => {
|
|
@@ -840,104 +1032,7 @@ function configureHost(options) {
|
|
|
840
1032
|
function placeholderImageUrl() {
|
|
841
1033
|
return placeholderUrl;
|
|
842
1034
|
}
|
|
843
|
-
var DEFAULT_BASE = "/api/admin/animations";
|
|
844
|
-
var BASE = DEFAULT_BASE;
|
|
845
|
-
function configureApi(options) {
|
|
846
|
-
if (options.baseUrl) BASE = options.baseUrl.replace(/\/+$/, "");
|
|
847
|
-
}
|
|
848
|
-
function apiBaseUrl() {
|
|
849
|
-
return BASE;
|
|
850
|
-
}
|
|
851
|
-
var revisions = /* @__PURE__ */ new Map();
|
|
852
|
-
var MissingAnimationRevisionError = class extends Error {
|
|
853
|
-
};
|
|
854
|
-
var AnimationStudioApiError = class extends Error {
|
|
855
|
-
};
|
|
856
|
-
function isRecord(value) {
|
|
857
|
-
return typeof value === "object" && value !== null;
|
|
858
|
-
}
|
|
859
|
-
async function readJson(res) {
|
|
860
|
-
if (!res.ok) {
|
|
861
|
-
const text = await res.text();
|
|
862
|
-
throw new AnimationStudioApiError(`HTTP ${res.status}: ${text}`);
|
|
863
|
-
}
|
|
864
|
-
return res.json();
|
|
865
|
-
}
|
|
866
|
-
function parseRevision(value) {
|
|
867
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
|
|
868
|
-
throw new TypeError("Animation revision is malformed");
|
|
869
|
-
}
|
|
870
|
-
return value;
|
|
871
|
-
}
|
|
872
|
-
function parseAnimationEnvelope(value) {
|
|
873
|
-
if (!isRecord(value)) throw new TypeError("Animation response is malformed");
|
|
874
|
-
return {
|
|
875
|
-
def: animationDocumentSchema.parse(value.def),
|
|
876
|
-
revision: parseRevision(value.revision)
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
async function listAnimations() {
|
|
880
|
-
const res = await fetch(BASE);
|
|
881
|
-
const data = await readJson(res);
|
|
882
|
-
if (!isRecord(data) || !Array.isArray(data.items))
|
|
883
|
-
throw new TypeError("Animation list response is malformed");
|
|
884
|
-
return data.items.map((item) => {
|
|
885
|
-
if (!isRecord(item) || typeof item.id !== "string" || typeof item.title !== "string" || typeof item.description !== "string") {
|
|
886
|
-
throw new TypeError("Animation summary is malformed");
|
|
887
|
-
}
|
|
888
|
-
return {
|
|
889
|
-
id: item.id,
|
|
890
|
-
title: item.title,
|
|
891
|
-
description: item.description,
|
|
892
|
-
...typeof item.updatedAt === "string" ? { updatedAt: item.updatedAt } : {}
|
|
893
|
-
};
|
|
894
|
-
});
|
|
895
|
-
}
|
|
896
|
-
async function loadAnimation(id) {
|
|
897
|
-
const res = await fetch(`${BASE}/${encodeURIComponent(id)}`);
|
|
898
|
-
const data = parseAnimationEnvelope(await readJson(res));
|
|
899
|
-
revisions.set(data.def.id, data.revision);
|
|
900
|
-
return data.def;
|
|
901
|
-
}
|
|
902
|
-
async function saveAnimation(def) {
|
|
903
|
-
const revision = revisions.get(def.id);
|
|
904
|
-
if (revision === void 0)
|
|
905
|
-
throw new MissingAnimationRevisionError(
|
|
906
|
-
`Animation '${def.id}' has no loaded revision`
|
|
907
|
-
);
|
|
908
|
-
const res = await fetch(`${BASE}/${encodeURIComponent(def.id)}`, {
|
|
909
|
-
method: "PUT",
|
|
910
|
-
headers: { "Content-Type": "application/json" },
|
|
911
|
-
body: JSON.stringify({ def, revision })
|
|
912
|
-
});
|
|
913
|
-
const data = parseAnimationEnvelope(await readJson(res));
|
|
914
|
-
revisions.set(data.def.id, data.revision);
|
|
915
|
-
return data.def;
|
|
916
|
-
}
|
|
917
|
-
async function createAnimation(id, title) {
|
|
918
|
-
const res = await fetch(BASE, {
|
|
919
|
-
method: "POST",
|
|
920
|
-
headers: { "Content-Type": "application/json" },
|
|
921
|
-
body: JSON.stringify({ id, title })
|
|
922
|
-
});
|
|
923
|
-
const data = parseAnimationEnvelope(await readJson(res));
|
|
924
|
-
revisions.set(data.def.id, data.revision);
|
|
925
|
-
return data.def;
|
|
926
|
-
}
|
|
927
|
-
async function deleteAnimation(id) {
|
|
928
|
-
const res = await fetch(`${BASE}/${encodeURIComponent(id)}`, {
|
|
929
|
-
method: "DELETE"
|
|
930
|
-
});
|
|
931
|
-
await readJson(res);
|
|
932
|
-
revisions.delete(id);
|
|
933
|
-
}
|
|
934
|
-
async function duplicateAnimation(sourceId, newId, newTitle) {
|
|
935
|
-
const source = await loadAnimation(sourceId);
|
|
936
|
-
const cloned = { ...source, id: newId, title: newTitle };
|
|
937
|
-
await createAnimation(newId, newTitle);
|
|
938
|
-
return await saveAnimation(cloned);
|
|
939
|
-
}
|
|
940
1035
|
|
|
941
|
-
export { addAppearance, addChapter, addEffect, addElement, apiBaseUrl, beginTransient, canRedo, canUndo, childIdsOf, configureApi, configureHost, createAnimation, deleteAnimation, deleteChapter, deleteEffect, deleteElement, duplicateAnimation, endTransient, findContainingGroup, getCurrentSnapshot, getCurrentTime, getDef, getHistory, getSelectedElementIds, getSelection, groupBbox, groupElements, isDirty, isDraft, isElementSelected, isGroup, jumpBack, jumpForward, listAnimations, loadAnimation, markClean, moveElementToEnd, moveElementToFront, moveGroupBy, placeholderImageUrl, promoteDraftToSaved, redo, registerDataUriAsset, registerExternalAsset, registerInlineAsset, removeAppearance, removeTrack, removeTrackKeyframe, reorderElement, resetHistory, saveAnimation, setCurrentTime, setDef, setDraft, setElementValueAtTime, setSelection, setTrackKeyframe, subscribe, toggleSelectionFor, undo, ungroupElement, uniqueChapterId, uniqueEffectId, uniqueElementId, updateAppearance, updateCanvas, updateChapter, updateDuration, updateEffect, updateElementBase, updateMeta, updateSettings };
|
|
942
|
-
//# sourceMappingURL=chunk-
|
|
943
|
-
//# sourceMappingURL=chunk-
|
|
1036
|
+
export { addAppearance, addChapter, addEffect, addElement, animationDocumentFileName, animationDocumentToJson, apiBaseUrl, beginTransient, canRedo, canUndo, childIdsOf, configureAnimationRepository, configureApi, configureHost, createAnimation, deleteAnimation, deleteChapter, deleteEffect, deleteElement, downloadAnimationJson, duplicateAnimation, endTransient, findContainingGroup, garbageCollectAnimationAssets, getCurrentSnapshot, getCurrentTime, getDef, getHistory, getSelectedElementIds, getSelection, groupBbox, groupElements, importAnimation, isDirty, isDraft, isElementSelected, isGroup, jumpBack, jumpForward, listAnimations, loadAnimation, markClean, moveElementToEnd, moveElementToFront, moveGroupBy, placeholderImageUrl, promoteDraftToSaved, redo, registerDataUriAsset, registerExternalAsset, registerInlineAsset, removeAppearance, removeTrack, removeTrackKeyframe, reorderElement, resetHistory, saveAnimation, setCurrentTime, setDef, setDraft, setElementValueAtTime, setSelection, setTrackKeyframe, subscribe, toggleSelectionFor, undo, ungroupElement, uniqueChapterId, uniqueEffectId, uniqueElementId, updateAppearance, updateCanvas, updateChapter, updateDuration, updateEffect, updateElementBase, updateLocales, updateMeta, updateSettings };
|
|
1037
|
+
//# sourceMappingURL=chunk-GMGB7NIL.js.map
|
|
1038
|
+
//# sourceMappingURL=chunk-GMGB7NIL.js.map
|