@kokoa/clotho-editor 0.1.4 → 0.3.0
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 +53 -1
- package/dist/{chunk-GMGB7NIL.js → chunk-N47HF4VB.js} +436 -20
- package/dist/chunk-N47HF4VB.js.map +1 -0
- package/dist/clotho-editor.css +425 -0
- package/dist/index.d.ts +199 -18
- package/dist/index.js +515 -9
- package/dist/index.js.map +1 -1
- package/dist/{main-NCPWDB2U.js → main-DC6D6RNQ.js} +937 -22
- package/dist/main-DC6D6RNQ.js.map +1 -0
- package/package.json +7 -3
- package/dist/chunk-GMGB7NIL.js.map +0 -1
- package/dist/main-NCPWDB2U.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { downloadAnimationJson, configureAnimationRepository } from './chunk-
|
|
2
|
-
export { addAppearance, addChapter, addEffect, addElement, animationDocumentFileName, animationDocumentToJson, apiBaseUrl, beginTransient, canRedo, canUndo, childIdsOf, configureAnimationRepository, configureApi, configureHost, deleteChapter, deleteEffect, deleteElement, downloadAnimationJson, endTransient, garbageCollectAnimationAssets, getCurrentSnapshot, getCurrentTime, getDef, getHistory, getSelectedElementIds, getSelection, groupElements, isDirty, isDraft, isElementSelected, isGroup, jumpBack, jumpForward, markClean, moveElementToEnd, moveElementToFront, placeholderImageUrl, promoteDraftToSaved, redo, registerDataUriAsset, registerExternalAsset, registerInlineAsset, removeAppearance, removeTrack, removeTrackKeyframe, reorderElement, resetHistory, setCurrentTime, setDef, setDraft, setElementValueAtTime, setSelection, setTrackKeyframe, subscribe, toggleSelectionFor, undo, ungroupElement, uniqueChapterId, uniqueEffectId, uniqueElementId, updateAppearance, updateCanvas, updateChapter, updateDuration, updateEffect, updateElementBase, updateLocales, updateMeta, updateSettings } from './chunk-
|
|
3
|
-
import { useState, useEffect, useRef } from 'react';
|
|
1
|
+
import { downloadAnimationJson, configureAnimationRepository } from './chunk-N47HF4VB.js';
|
|
2
|
+
export { addAppearance, addCameraFocus, addChapter, addCheckpoint, addEffect, addElement, animationDocumentFileName, animationDocumentToJson, apiBaseUrl, beginTransient, cameraControlAt, canRedo, canUndo, childIdsOf, clearCamera, configureAnimationRepository, configureApi, configureHost, createEditorPluginContext, createLayout, deleteCameraFocus, deleteChapter, deleteCheckpoint, deleteEffect, deleteElement, detachFromLayout, downloadAnimationJson, endTransient, findLayoutCollisions, garbageCollectAnimationAssets, getCamera, getCurrentSnapshot, getCurrentTime, getDef, getHistory, getSelectedElementIds, getSelection, groupElements, hasCamera, isDirty, isDraft, isElementSelected, isGroup, jumpBack, jumpForward, layoutIdsFor, markClean, mountEditorPlugins, moveCameraKeyframe, moveElementToEnd, moveElementToFront, placeholderImageUrl, promoteDraftToSaved, redo, registerDataUriAsset, registerExternalAsset, registerInlineAsset, removeAppearance, removeCameraKeyframe, removeCameraTrack, removeTrack, removeTrackKeyframe, reorderElement, resetHistory, setCameraKeyframe, setCameraStrokeScaling, setCurrentTime, setDef, setDraft, setElementValueAtTime, setSelection, setTrackKeyframe, subscribe, toggleSelectionFor, undo, ungroupElement, uniqueChapterId, uniqueCheckpointId, uniqueEffectId, uniqueElementId, updateAppearance, updateCameraFocus, updateCanvas, updateChapter, updateCheckpoint, updateData, updateDuration, updateEffect, updateElementBase, updateLocales, updateMeta, updateResponsive, updateSettings, validateEditorPlugin } from './chunk-N47HF4VB.js';
|
|
3
|
+
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
4
4
|
import { usePlayer, AnimationStage } from '@kokoa/clotho/react';
|
|
5
5
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
|
-
import { animationDocumentSchema } from '@kokoa/clotho';
|
|
6
|
+
import { defineStory, animationDocumentSchema, compileSceneDependencyPlan, buildScene, lintDocument, autofixDocument, compileResponsiveStage } from '@kokoa/clotho';
|
|
7
|
+
import { animationSampleTimes, expectAnimation, snapshotAnimationMatrix, animationFailureReport, AnimationAssertionError } from '@kokoa/clotho/testing';
|
|
8
|
+
import { renderDocumentToSvg } from '@kokoa/clotho/svg';
|
|
7
9
|
|
|
8
10
|
function elementsOf(def) {
|
|
9
11
|
return def.elements;
|
|
@@ -54,6 +56,7 @@ function Studio({ initial, onSave }) {
|
|
|
54
56
|
rotation: 0,
|
|
55
57
|
content: "New",
|
|
56
58
|
translations: {},
|
|
59
|
+
references: {},
|
|
57
60
|
fontSize: 24,
|
|
58
61
|
color: "#4f46e5",
|
|
59
62
|
textAnchor: "start",
|
|
@@ -215,6 +218,250 @@ function Studio({ initial, onSave }) {
|
|
|
215
218
|
] })
|
|
216
219
|
] });
|
|
217
220
|
}
|
|
221
|
+
|
|
222
|
+
// src/template-editor-plugin.ts
|
|
223
|
+
function initialParameterValue(schema) {
|
|
224
|
+
if (schema.default !== void 0) return structuredClone(schema.default);
|
|
225
|
+
if (schema.type === "string") return "";
|
|
226
|
+
if (schema.type === "number") return schema.min ?? 0;
|
|
227
|
+
if (schema.type === "boolean") return false;
|
|
228
|
+
if (schema.type === "enum") return schema.values[0];
|
|
229
|
+
if (schema.type === "array") return [];
|
|
230
|
+
return Object.fromEntries(
|
|
231
|
+
Object.entries(schema.properties).map(([key, child]) => [
|
|
232
|
+
key,
|
|
233
|
+
initialParameterValue(child)
|
|
234
|
+
])
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
function parameterInputValue(schema, raw) {
|
|
238
|
+
if (schema.type === "boolean") return Boolean(raw);
|
|
239
|
+
if (schema.type === "number") return Number(raw);
|
|
240
|
+
if (schema.type === "array" || schema.type === "object")
|
|
241
|
+
return JSON.parse(String(raw));
|
|
242
|
+
return String(raw);
|
|
243
|
+
}
|
|
244
|
+
function downloadReference(reference) {
|
|
245
|
+
const blob = new Blob([JSON.stringify(reference, null, 2)], {
|
|
246
|
+
type: "application/json"
|
|
247
|
+
});
|
|
248
|
+
const url = URL.createObjectURL(blob);
|
|
249
|
+
const anchor = document.createElement("a");
|
|
250
|
+
anchor.href = url;
|
|
251
|
+
anchor.download = `${reference.templateId.replace(/[^a-z0-9._-]/gi, "-")}.template.json`;
|
|
252
|
+
anchor.click();
|
|
253
|
+
URL.revokeObjectURL(url);
|
|
254
|
+
}
|
|
255
|
+
function field(key, schema, value, onChange) {
|
|
256
|
+
const label = document.createElement("label");
|
|
257
|
+
label.className = "studio-field studio-template-field";
|
|
258
|
+
const title = document.createElement("span");
|
|
259
|
+
title.textContent = key;
|
|
260
|
+
if (schema.description) title.title = schema.description;
|
|
261
|
+
label.append(title);
|
|
262
|
+
let control;
|
|
263
|
+
if (schema.type === "enum") {
|
|
264
|
+
control = document.createElement("select");
|
|
265
|
+
for (const option of schema.values) {
|
|
266
|
+
const element = document.createElement("option");
|
|
267
|
+
element.value = option;
|
|
268
|
+
element.textContent = option;
|
|
269
|
+
control.append(element);
|
|
270
|
+
}
|
|
271
|
+
control.value = String(value);
|
|
272
|
+
} else if (schema.type === "array" || schema.type === "object") {
|
|
273
|
+
control = document.createElement("textarea");
|
|
274
|
+
control.rows = 3;
|
|
275
|
+
control.value = JSON.stringify(value, null, 2);
|
|
276
|
+
} else {
|
|
277
|
+
control = document.createElement("input");
|
|
278
|
+
control.type = schema.type === "boolean" ? "checkbox" : schema.type;
|
|
279
|
+
if (schema.type === "boolean") control.checked = Boolean(value);
|
|
280
|
+
else control.value = String(value);
|
|
281
|
+
if (schema.type === "number") {
|
|
282
|
+
if (schema.min !== void 0) control.min = String(schema.min);
|
|
283
|
+
if (schema.max !== void 0) control.max = String(schema.max);
|
|
284
|
+
control.step = schema.integer ? "1" : "any";
|
|
285
|
+
}
|
|
286
|
+
if (schema.type === "string") {
|
|
287
|
+
if (schema.minLength !== void 0) control.minLength = schema.minLength;
|
|
288
|
+
if (schema.maxLength !== void 0) control.maxLength = schema.maxLength;
|
|
289
|
+
if (schema.pattern !== void 0) control.pattern = schema.pattern;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
control.dataset.templateParameter = key;
|
|
293
|
+
const update = () => {
|
|
294
|
+
try {
|
|
295
|
+
const raw = control instanceof HTMLInputElement && control.type === "checkbox" ? control.checked : control.value;
|
|
296
|
+
onChange(parameterInputValue(schema, raw));
|
|
297
|
+
control.setCustomValidity("");
|
|
298
|
+
} catch {
|
|
299
|
+
control.setCustomValidity("\uC62C\uBC14\uB978 JSON\uC744 \uC785\uB825\uD558\uC138\uC694.");
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
control.addEventListener(
|
|
303
|
+
schema.type === "enum" || schema.type === "boolean" ? "change" : "input",
|
|
304
|
+
update
|
|
305
|
+
);
|
|
306
|
+
label.append(control);
|
|
307
|
+
return label;
|
|
308
|
+
}
|
|
309
|
+
function createTemplateEditorPlugin(templates) {
|
|
310
|
+
return {
|
|
311
|
+
manifest: {
|
|
312
|
+
id: "dev.clotho.templates",
|
|
313
|
+
name: "Template parameters",
|
|
314
|
+
capabilities: ["editor"],
|
|
315
|
+
editor: { panels: ["parameters"] }
|
|
316
|
+
},
|
|
317
|
+
panels: {
|
|
318
|
+
parameters: {
|
|
319
|
+
id: "parameters",
|
|
320
|
+
label: "Template parameters",
|
|
321
|
+
mount(container, context) {
|
|
322
|
+
if (templates.length === 0) return;
|
|
323
|
+
container.classList.add(
|
|
324
|
+
"studio-tools-section",
|
|
325
|
+
"studio-template-panel"
|
|
326
|
+
);
|
|
327
|
+
const heading = document.createElement("div");
|
|
328
|
+
heading.className = "studio-tools-title";
|
|
329
|
+
heading.textContent = "Template";
|
|
330
|
+
const select = document.createElement("select");
|
|
331
|
+
select.className = "studio-template-select";
|
|
332
|
+
for (const template of templates) {
|
|
333
|
+
const option = document.createElement("option");
|
|
334
|
+
option.value = template.id;
|
|
335
|
+
option.textContent = template.id;
|
|
336
|
+
select.append(option);
|
|
337
|
+
}
|
|
338
|
+
const form = document.createElement("div");
|
|
339
|
+
const status = document.createElement("p");
|
|
340
|
+
status.className = "studio-props-empty studio-template-status";
|
|
341
|
+
const actions = document.createElement("div");
|
|
342
|
+
actions.className = "studio-align-row";
|
|
343
|
+
const standalone = document.createElement("button");
|
|
344
|
+
standalone.className = "studio-btn";
|
|
345
|
+
standalone.textContent = "Standalone JSON";
|
|
346
|
+
const reference = document.createElement("button");
|
|
347
|
+
reference.className = "studio-btn";
|
|
348
|
+
reference.textContent = "Template \uCC38\uC870";
|
|
349
|
+
actions.append(standalone, reference);
|
|
350
|
+
container.append(heading, select, form, status, actions);
|
|
351
|
+
let active = templates[0];
|
|
352
|
+
let values = {};
|
|
353
|
+
let previewDocument = context.getDocument();
|
|
354
|
+
const rebuild = () => {
|
|
355
|
+
try {
|
|
356
|
+
previewDocument = active.instantiate(values);
|
|
357
|
+
context.replaceDocument(previewDocument);
|
|
358
|
+
status.textContent = "parameter\uAC00 \uBBF8\uB9AC\uBCF4\uAE30\uC5D0 \uBC18\uC601\uB418\uC5C8\uC2B5\uB2C8\uB2E4.";
|
|
359
|
+
status.removeAttribute("data-error");
|
|
360
|
+
} catch (error) {
|
|
361
|
+
const issues = error.issues;
|
|
362
|
+
status.textContent = issues ? issues.map(({ path, message }) => `${path}: ${message}`).join(" \xB7 ") : String(error);
|
|
363
|
+
status.dataset.error = "true";
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
const render = () => {
|
|
367
|
+
values = Object.fromEntries(
|
|
368
|
+
Object.entries(active.parameters).map(([key, schema]) => [
|
|
369
|
+
key,
|
|
370
|
+
initialParameterValue(schema)
|
|
371
|
+
])
|
|
372
|
+
);
|
|
373
|
+
form.replaceChildren(
|
|
374
|
+
...Object.entries(active.parameters).map(
|
|
375
|
+
([key, schema]) => field(key, schema, values[key], (value) => {
|
|
376
|
+
values[key] = value;
|
|
377
|
+
rebuild();
|
|
378
|
+
})
|
|
379
|
+
)
|
|
380
|
+
);
|
|
381
|
+
rebuild();
|
|
382
|
+
};
|
|
383
|
+
select.addEventListener("change", () => {
|
|
384
|
+
active = templates.find(({ id }) => id === select.value) ?? templates[0];
|
|
385
|
+
render();
|
|
386
|
+
});
|
|
387
|
+
standalone.addEventListener(
|
|
388
|
+
"click",
|
|
389
|
+
() => downloadAnimationJson(previewDocument)
|
|
390
|
+
);
|
|
391
|
+
reference.addEventListener(
|
|
392
|
+
"click",
|
|
393
|
+
() => downloadReference(active.reference(values))
|
|
394
|
+
);
|
|
395
|
+
render();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function replaceStoryNodeDocument(manifest, nodeId, document2) {
|
|
402
|
+
return defineStory({
|
|
403
|
+
...manifest,
|
|
404
|
+
nodes: manifest.nodes.map(
|
|
405
|
+
(node) => node.id === nodeId ? { ...node, document: document2 } : node
|
|
406
|
+
)
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
function appendStoryEdge(manifest, edge) {
|
|
410
|
+
return defineStory({ ...manifest, edges: [...manifest.edges, edge] });
|
|
411
|
+
}
|
|
412
|
+
function createStoryEditorPlugin(options) {
|
|
413
|
+
let manifest = structuredClone(options.manifest);
|
|
414
|
+
let activeNode = manifest.initialNode;
|
|
415
|
+
return {
|
|
416
|
+
manifest: {
|
|
417
|
+
id: "dev.clotho.story",
|
|
418
|
+
name: "Story Graph",
|
|
419
|
+
capabilities: ["editor"],
|
|
420
|
+
editor: { panels: ["story-graph"] }
|
|
421
|
+
},
|
|
422
|
+
panels: {
|
|
423
|
+
"story-graph": {
|
|
424
|
+
id: "story-graph",
|
|
425
|
+
label: "Story Graph",
|
|
426
|
+
mount(container, context) {
|
|
427
|
+
const render = () => {
|
|
428
|
+
container.innerHTML = `<div class="studio-tools-section"><div class="studio-tools-title">Story Graph</div><div class="studio-story-nodes">${manifest.nodes.map((node) => `<button type="button" class="studio-btn${node.id === activeNode ? " is-active" : ""}" data-story-node="${escapeAttribute(node.id)}">${escapeHtml(node.title || node.id)}</button>`).join("")}</div><div class="studio-tools-hint">${manifest.edges.map((edge) => `${escapeHtml(edge.from)} \u2192 ${escapeHtml(edge.to)}${edge.label ? ` \xB7 ${escapeHtml(edge.label)}` : ""}`).join("<br>") || "\uC5F0\uACB0\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."}</div></div>`;
|
|
429
|
+
};
|
|
430
|
+
const click = (event) => {
|
|
431
|
+
const button = event.target.closest(
|
|
432
|
+
"[data-story-node]"
|
|
433
|
+
);
|
|
434
|
+
const nextId = button?.dataset.storyNode;
|
|
435
|
+
if (!nextId || nextId === activeNode) return;
|
|
436
|
+
manifest = replaceStoryNodeDocument(
|
|
437
|
+
manifest,
|
|
438
|
+
activeNode,
|
|
439
|
+
context.getDocument()
|
|
440
|
+
);
|
|
441
|
+
const node = manifest.nodes.find(({ id }) => id === nextId);
|
|
442
|
+
if (!node) return;
|
|
443
|
+
activeNode = nextId;
|
|
444
|
+
context.replaceDocument(node.document);
|
|
445
|
+
options.onChange?.(structuredClone(manifest));
|
|
446
|
+
render();
|
|
447
|
+
};
|
|
448
|
+
container.addEventListener("click", click);
|
|
449
|
+
render();
|
|
450
|
+
return () => container.removeEventListener("click", click);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function escapeHtml(value) {
|
|
457
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
458
|
+
}
|
|
459
|
+
function escapeAttribute(value) {
|
|
460
|
+
return escapeHtml(value).replaceAll('"', """);
|
|
461
|
+
}
|
|
462
|
+
var EMPTY_EDITOR_PLUGINS = [];
|
|
463
|
+
var EMPTY_TEMPLATES = [];
|
|
464
|
+
var DENY_PLUGIN_PERMISSIONS = () => ({});
|
|
218
465
|
var CANVAS_PRESETS = [
|
|
219
466
|
{ id: "800x500", label: "8:5", sub: "800\xD7500", w: 32, h: 20 },
|
|
220
467
|
{ id: "1280x720", label: "16:9", sub: "HD", w: 32, h: 18 },
|
|
@@ -238,11 +485,13 @@ var SKELETON = `
|
|
|
238
485
|
<header class="studio-header">
|
|
239
486
|
<h1 id="studio-editor-title" class="studio-header-title">Clotho Editor</h1>
|
|
240
487
|
<div class="studio-header-actions">
|
|
488
|
+
<span class="studio-plugin-toolbar" data-editor-plugin-slot="toolbar"></span>
|
|
241
489
|
<button type="button" id="studio-open" class="studio-btn" aria-label="\uC800\uC7A5\uB41C \uC560\uB2C8\uBA54\uC774\uC158 \uC5F4\uAE30">\u{1F4C1} \uC5F4\uAE30</button>
|
|
242
490
|
<button type="button" id="studio-new" class="studio-btn" aria-label="\uC0C8 \uC560\uB2C8\uBA54\uC774\uC158 \uB9CC\uB4E4\uAE30">\uFF0B \uC0C8 \uC560\uB2C8\uBA54\uC774\uC158</button>
|
|
243
491
|
<button type="button" id="studio-undo" class="studio-btn studio-btn-icon" aria-label="\uC2E4\uD589 \uCDE8\uC18C" title="\uC2E4\uD589 \uCDE8\uC18C (\u2318Z)" disabled>\u21B6</button>
|
|
244
492
|
<button type="button" id="studio-redo" class="studio-btn studio-btn-icon" aria-label="\uB2E4\uC2DC \uC2E4\uD589" title="\uB2E4\uC2DC \uC2E4\uD589 (\u2318\u21E7Z / \u2318Y)" disabled>\u21B7</button>
|
|
245
493
|
<button type="button" id="studio-grid-toggle" class="studio-btn studio-btn-grid" aria-label="\uACA9\uC790 + \uC2A4\uB0C5" title="\uACA9\uC790 + \uC2A4\uB0C5 (G)" aria-pressed="false"><span class="studio-btn-grid-icon">\u229E</span><span class="studio-btn-grid-label" id="studio-grid-label">\uACA9\uC790 \uB054</span></button>
|
|
494
|
+
<button type="button" id="studio-camera-frame-toggle" class="studio-btn studio-btn-grid" aria-label="\uCE74\uBA54\uB77C \uC601\uC5ED \uD45C\uC2DC" title="\uCE94\uBC84\uC2A4\uC5D0 \uCE74\uBA54\uB77C\uAC00 \uBCF4\uC5EC\uC8FC\uB294 \uC601\uC5ED\uC744 \uD45C\uC2DC\uD569\uB2C8\uB2E4" aria-pressed="true"><span class="studio-btn-grid-icon">\u{1F3A5}</span><span class="studio-btn-grid-label" id="studio-camera-frame-label">\uCE74\uBA54\uB77C \uC601\uC5ED</span></button>
|
|
246
495
|
<input type="text" id="studio-title" class="studio-title-input" placeholder="\uC81C\uBAA9" aria-label="\uC560\uB2C8\uBA54\uC774\uC158 \uC81C\uBAA9" disabled />
|
|
247
496
|
<span id="studio-id-display" class="studio-id-display"></span>
|
|
248
497
|
<span id="studio-status" class="studio-status" aria-live="polite">\uB300\uAE30 \uC911</span>
|
|
@@ -270,6 +519,7 @@ var SKELETON = `
|
|
|
270
519
|
<div class="studio-tool-with-option"><button type="button" class="studio-tool-btn" data-add-element="polygon" title="\uB2E4\uAC01\uD615 (Y)" aria-label="\uB2E4\uAC01\uD615 \uB3C4\uAD6C"><span>\u2B22 Polygon</span><kbd>Y</kbd></button><label>\uBCC0 <input type="number" id="studio-polygon-sides" min="3" max="24" value="6" aria-label="\uB2E4\uAC01\uD615\uC758 \uBCC0 \uAC1C\uC218" /></label></div>
|
|
271
520
|
<button type="button" class="studio-tool-btn" id="studio-open-icons" title="\uC544\uC774\uCF58 \uB77C\uC774\uBE0C\uB7EC\uB9AC" aria-label="\uC544\uC774\uCF58 \uB77C\uC774\uBE0C\uB7EC\uB9AC \uCD94\uAC00">\u{1F3A8} Icons</button>
|
|
272
521
|
</div>
|
|
522
|
+
<div class="studio-plugin-panels" data-editor-plugin-slot="panel"></div>
|
|
273
523
|
<div class="studio-tools-section">
|
|
274
524
|
<div class="studio-tools-title">\uCE94\uBC84\uC2A4 \uD06C\uAE30</div>
|
|
275
525
|
<div class="studio-canvas-size-row">
|
|
@@ -311,6 +561,7 @@ var SKELETON = `
|
|
|
311
561
|
<div id="studio-props-content" class="studio-props-content">
|
|
312
562
|
<p class="studio-props-empty">\uC694\uC18C \uB610\uB294 step \uC744 \uC120\uD0DD\uD558\uC138\uC694.</p>
|
|
313
563
|
</div>
|
|
564
|
+
<div class="studio-plugin-inspectors" data-editor-plugin-slot="inspector"></div>
|
|
314
565
|
</aside>
|
|
315
566
|
</div>
|
|
316
567
|
|
|
@@ -426,24 +677,58 @@ function StudioMount({
|
|
|
426
677
|
initialId,
|
|
427
678
|
repository,
|
|
428
679
|
editorTitle = "Clotho Editor",
|
|
429
|
-
resolveImage
|
|
680
|
+
resolveImage,
|
|
681
|
+
importDocument,
|
|
682
|
+
plugins = EMPTY_EDITOR_PLUGINS,
|
|
683
|
+
resolvePluginPermissions = DENY_PLUGIN_PERMISSIONS,
|
|
684
|
+
templates = EMPTY_TEMPLATES,
|
|
685
|
+
story
|
|
430
686
|
}) {
|
|
431
687
|
const inited = useRef(false);
|
|
688
|
+
const mountedPlugins = useMemo(
|
|
689
|
+
() => [
|
|
690
|
+
...templates.length > 0 ? [createTemplateEditorPlugin(templates)] : [],
|
|
691
|
+
...story ? [createStoryEditorPlugin(story)] : [],
|
|
692
|
+
...plugins
|
|
693
|
+
],
|
|
694
|
+
[plugins, templates, story]
|
|
695
|
+
);
|
|
696
|
+
const permissionResolver = useMemo(
|
|
697
|
+
() => (plugin) => plugin.id === "dev.clotho.templates" || plugin.id === "dev.clotho.story" ? { ui: true, documentRead: true, documentWrite: true } : resolvePluginPermissions(plugin),
|
|
698
|
+
[resolvePluginPermissions]
|
|
699
|
+
);
|
|
432
700
|
useEffect(() => {
|
|
433
701
|
if (inited.current) return;
|
|
434
702
|
inited.current = true;
|
|
435
703
|
if (repository) configureAnimationRepository(repository);
|
|
436
704
|
let disposed = false;
|
|
437
|
-
|
|
705
|
+
let cleanup;
|
|
706
|
+
void import('./main-DC6D6RNQ.js').then(({ initStudio }) => {
|
|
438
707
|
if (disposed) return;
|
|
439
|
-
initStudio({
|
|
708
|
+
cleanup = initStudio({
|
|
709
|
+
initialId,
|
|
710
|
+
editorTitle,
|
|
711
|
+
resolveImage,
|
|
712
|
+
importDocument,
|
|
713
|
+
plugins: mountedPlugins,
|
|
714
|
+
resolvePluginPermissions: permissionResolver
|
|
715
|
+
});
|
|
440
716
|
});
|
|
441
717
|
return () => {
|
|
442
718
|
disposed = true;
|
|
719
|
+
cleanup?.();
|
|
443
720
|
document.body.classList.remove("editor-active");
|
|
444
721
|
document.documentElement.classList.remove("editor-active");
|
|
445
722
|
};
|
|
446
|
-
}, [
|
|
723
|
+
}, [
|
|
724
|
+
editorTitle,
|
|
725
|
+
importDocument,
|
|
726
|
+
initialId,
|
|
727
|
+
mountedPlugins,
|
|
728
|
+
repository,
|
|
729
|
+
resolveImage,
|
|
730
|
+
permissionResolver
|
|
731
|
+
]);
|
|
447
732
|
return /* @__PURE__ */ jsx("section", { className: "studio-shell w-full", "data-pagefind-ignore": "all", children: /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: SKELETON } }) });
|
|
448
733
|
}
|
|
449
734
|
function browserStorage() {
|
|
@@ -531,7 +816,228 @@ function createLocalStorageRepository(options = {}) {
|
|
|
531
816
|
}
|
|
532
817
|
};
|
|
533
818
|
}
|
|
819
|
+
function downloadReport(html) {
|
|
820
|
+
const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
|
|
821
|
+
const link = document.createElement("a");
|
|
822
|
+
link.href = url;
|
|
823
|
+
link.download = "clotho-animation-qa.html";
|
|
824
|
+
link.click();
|
|
825
|
+
URL.revokeObjectURL(url);
|
|
826
|
+
}
|
|
827
|
+
function createVisualRegressionPlugin() {
|
|
828
|
+
return {
|
|
829
|
+
manifest: {
|
|
830
|
+
id: "dev.clotho.visual-regression",
|
|
831
|
+
name: "Animation QA",
|
|
832
|
+
capabilities: ["editor"],
|
|
833
|
+
editor: { inspectors: ["animation-qa"] }
|
|
834
|
+
},
|
|
835
|
+
inspectors: {
|
|
836
|
+
"animation-qa": {
|
|
837
|
+
id: "animation-qa",
|
|
838
|
+
label: "Animation QA",
|
|
839
|
+
mount(container, context) {
|
|
840
|
+
container.classList.add("studio-qa-panel");
|
|
841
|
+
const title = document.createElement("strong");
|
|
842
|
+
title.textContent = "Animation QA";
|
|
843
|
+
const run = document.createElement("button");
|
|
844
|
+
run.type = "button";
|
|
845
|
+
run.className = "studio-btn";
|
|
846
|
+
run.textContent = "\uC804\uCCB4 frame \uAC80\uC0AC";
|
|
847
|
+
const status = document.createElement("p");
|
|
848
|
+
status.className = "studio-props-empty";
|
|
849
|
+
container.append(title, run, status);
|
|
850
|
+
run.addEventListener("click", () => {
|
|
851
|
+
const animation = context.getDocument();
|
|
852
|
+
const errors = [];
|
|
853
|
+
for (const time of animationSampleTimes(animation)) {
|
|
854
|
+
for (const element of animation.elements) {
|
|
855
|
+
if (!["rect", "circle", "image", "code"].includes(element.type))
|
|
856
|
+
continue;
|
|
857
|
+
try {
|
|
858
|
+
expectAnimation(animation).at(time).insideCanvas(element.id);
|
|
859
|
+
} catch (error) {
|
|
860
|
+
if (error instanceof AnimationAssertionError)
|
|
861
|
+
errors.push(error);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const snapshots = snapshotAnimationMatrix(animation);
|
|
866
|
+
if (errors.length === 0) {
|
|
867
|
+
status.textContent = `${snapshots.length}\uAC1C locale\xB7theme frame\uC744 \uAC80\uC0AC\uD588\uC2B5\uB2C8\uB2E4. \uBB38\uC81C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`;
|
|
868
|
+
status.removeAttribute("data-error");
|
|
869
|
+
} else {
|
|
870
|
+
status.textContent = `${errors.length}\uAC1C \uBB38\uC81C\uB97C \uBC1C\uACAC\uD588\uC2B5\uB2C8\uB2E4. HTML report\uB97C \uB0B4\uB824\uBC1B\uC2B5\uB2C8\uB2E4.`;
|
|
871
|
+
status.dataset.error = "true";
|
|
872
|
+
downloadReport(animationFailureReport(errors));
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
var RESPONSIVE_VIEWPORTS = [
|
|
881
|
+
{ id: "compact", label: "\uBAA8\uBC14\uC77C", width: 375 },
|
|
882
|
+
{ id: "regular", label: "\uBCF8\uBB38", width: 768 },
|
|
883
|
+
{ id: "wide", label: "\uB113\uC740 \uD654\uBA74", width: 1280 }
|
|
884
|
+
];
|
|
885
|
+
function addDefaultResponsiveVariants(document2) {
|
|
886
|
+
if (document2.responsive?.length) return document2;
|
|
887
|
+
return {
|
|
888
|
+
...document2,
|
|
889
|
+
responsive: [
|
|
890
|
+
{
|
|
891
|
+
id: "compact",
|
|
892
|
+
minWidth: 0,
|
|
893
|
+
maxWidth: 479,
|
|
894
|
+
chapterListPosition: "bottom",
|
|
895
|
+
elementOverrides: {}
|
|
896
|
+
},
|
|
897
|
+
{ id: "regular", minWidth: 480, maxWidth: 959, elementOverrides: {} },
|
|
898
|
+
{ id: "wide", minWidth: 960, elementOverrides: {} }
|
|
899
|
+
]
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
function createResponsiveInspectorPlugin() {
|
|
903
|
+
return {
|
|
904
|
+
manifest: {
|
|
905
|
+
id: "dev.clotho.responsive",
|
|
906
|
+
name: "Responsive Stage",
|
|
907
|
+
capabilities: ["editor"],
|
|
908
|
+
editor: { inspectors: ["responsive"] }
|
|
909
|
+
},
|
|
910
|
+
inspectors: {
|
|
911
|
+
responsive: {
|
|
912
|
+
id: "responsive",
|
|
913
|
+
label: "Responsive Stage",
|
|
914
|
+
mount(container, context) {
|
|
915
|
+
const render = () => {
|
|
916
|
+
const document2 = context.getDocument();
|
|
917
|
+
container.innerHTML = `<div class="studio-props-header"><span class="studio-props-header-title">Responsive Stage</span></div><button type="button" class="studio-btn" data-responsive-defaults>\uAE30\uBCF8 breakpoint \uB9CC\uB4E4\uAE30</button><div class="studio-responsive-grid">${RESPONSIVE_VIEWPORTS.map((viewport) => `<figure><figcaption>${viewport.label} \xB7 ${viewport.width}px</figcaption>${renderDocumentToSvg(compileResponsiveStage(document2, viewport.width), 0)}</figure>`).join("")}</div>`;
|
|
918
|
+
};
|
|
919
|
+
const click = (event) => {
|
|
920
|
+
if (!event.target.closest(
|
|
921
|
+
"[data-responsive-defaults]"
|
|
922
|
+
))
|
|
923
|
+
return;
|
|
924
|
+
context.replaceDocument(
|
|
925
|
+
addDefaultResponsiveVariants(context.getDocument())
|
|
926
|
+
);
|
|
927
|
+
render();
|
|
928
|
+
};
|
|
929
|
+
container.addEventListener("click", click);
|
|
930
|
+
render();
|
|
931
|
+
return () => container.removeEventListener("click", click);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
function profileAnimation(document2, sampleCount = 30, frameBudgetMs = 16.7) {
|
|
938
|
+
const plan = compileSceneDependencyPlan(document2);
|
|
939
|
+
const samples = [];
|
|
940
|
+
for (let index = 0; index < sampleCount; index += 1) {
|
|
941
|
+
const time = sampleCount === 1 ? 0 : document2.duration * index / (sampleCount - 1);
|
|
942
|
+
const started = performance.now();
|
|
943
|
+
buildScene(document2, time);
|
|
944
|
+
samples.push(performance.now() - started);
|
|
945
|
+
}
|
|
946
|
+
const totalMs = samples.reduce((sum, value) => sum + value, 0);
|
|
947
|
+
const maxMs = Math.max(0, ...samples);
|
|
948
|
+
return {
|
|
949
|
+
elementCount: plan.elementCount,
|
|
950
|
+
trackCount: plan.trackCount,
|
|
951
|
+
keyframeCount: plan.keyframeCount,
|
|
952
|
+
sampleCount,
|
|
953
|
+
totalMs,
|
|
954
|
+
averageMs: totalMs / Math.max(1, sampleCount),
|
|
955
|
+
maxMs,
|
|
956
|
+
overBudget: maxMs > frameBudgetMs
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
function createPerformanceProfilerPlugin() {
|
|
960
|
+
return {
|
|
961
|
+
manifest: {
|
|
962
|
+
id: "dev.clotho.performance",
|
|
963
|
+
name: "Scene Profiler",
|
|
964
|
+
capabilities: ["editor"],
|
|
965
|
+
editor: { inspectors: ["performance"] }
|
|
966
|
+
},
|
|
967
|
+
inspectors: {
|
|
968
|
+
performance: {
|
|
969
|
+
id: "performance",
|
|
970
|
+
label: "Scene Profiler",
|
|
971
|
+
mount(container, context) {
|
|
972
|
+
const run = () => {
|
|
973
|
+
const profile = profileAnimation(context.getDocument());
|
|
974
|
+
container.innerHTML = `<div class="studio-props-header"><span class="studio-props-header-title">Scene Profiler</span></div><dl class="studio-profile"><div><dt>Elements</dt><dd>${profile.elementCount}</dd></div><div><dt>Tracks / keyframes</dt><dd>${profile.trackCount} / ${profile.keyframeCount}</dd></div><div><dt>\uD3C9\uADE0 / \uCD5C\uB300</dt><dd>${profile.averageMs.toFixed(2)} / ${profile.maxMs.toFixed(2)} ms</dd></div></dl><p class="studio-template-status" data-error="${profile.overBudget}">${profile.overBudget ? "16.7ms frame budget\uC744 \uB118\uC5C8\uC2B5\uB2C8\uB2E4." : "60fps frame budget \uC548\uC5D0 \uC788\uC2B5\uB2C8\uB2E4."}</p><button type="button" class="studio-btn" data-run-profiler>\uB2E4\uC2DC \uCE21\uC815</button>`;
|
|
975
|
+
};
|
|
976
|
+
const click = (event) => {
|
|
977
|
+
if (event.target.closest("[data-run-profiler]"))
|
|
978
|
+
run();
|
|
979
|
+
};
|
|
980
|
+
container.addEventListener("click", click);
|
|
981
|
+
run();
|
|
982
|
+
return () => container.removeEventListener("click", click);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
function createLinterPlugin() {
|
|
989
|
+
return {
|
|
990
|
+
manifest: {
|
|
991
|
+
id: "dev.clotho.linter",
|
|
992
|
+
name: "Clotho Linter",
|
|
993
|
+
capabilities: ["editor"],
|
|
994
|
+
editor: { inspectors: ["linter"] }
|
|
995
|
+
},
|
|
996
|
+
inspectors: {
|
|
997
|
+
linter: {
|
|
998
|
+
id: "linter",
|
|
999
|
+
label: "Clotho Linter",
|
|
1000
|
+
mount(container, context) {
|
|
1001
|
+
let selectedIndex = 0;
|
|
1002
|
+
const render = () => {
|
|
1003
|
+
const document2 = context.getDocument();
|
|
1004
|
+
const issues = lintDocument(document2);
|
|
1005
|
+
const selected = issues[selectedIndex] ?? issues[0];
|
|
1006
|
+
const fixed = autofixDocument(document2).document;
|
|
1007
|
+
container.innerHTML = `<div class="studio-props-header"><span class="studio-props-header-title">Clotho Linter (${issues.length})</span><button type="button" class="studio-btn" data-lint-fix ${issues.some(({ fixable }) => fixable) ? "" : "disabled"}>\uC548\uC804\uD55C \uC218\uC815 \uC801\uC6A9</button></div><ol class="studio-lint-list">${issues.map((issue, index) => `<li><button type="button" data-lint-index="${index}" data-element-id="${issue.elementId ?? ""}"><strong>${escapeHtml2(issue.ruleId)}</strong><span>${escapeHtml2(issue.message)}</span>${issue.fixable ? "<em>\uC790\uB3D9 \uC218\uC815</em>" : ""}</button></li>`).join("") || "<li>\uBB38\uC81C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</li>"}</ol>${selected ? `<div class="studio-lint-compare"><figure><figcaption>\uD604\uC7AC</figcaption>${renderDocumentToSvg(document2, selected.time ?? 0)}</figure><figure><figcaption>\uC218\uC815 \uD6C4</figcaption>${renderDocumentToSvg(fixed, selected.time ?? 0)}</figure></div>` : ""}`;
|
|
1008
|
+
};
|
|
1009
|
+
const click = (event) => {
|
|
1010
|
+
const target = event.target;
|
|
1011
|
+
if (target.closest("[data-lint-fix]")) {
|
|
1012
|
+
context.replaceDocument(
|
|
1013
|
+
autofixDocument(context.getDocument()).document
|
|
1014
|
+
);
|
|
1015
|
+
selectedIndex = 0;
|
|
1016
|
+
render();
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
const issue = target.closest("[data-lint-index]");
|
|
1020
|
+
if (!issue) return;
|
|
1021
|
+
selectedIndex = Number(issue.dataset.lintIndex);
|
|
1022
|
+
if (issue.dataset.elementId)
|
|
1023
|
+
context.setSelection({
|
|
1024
|
+
kind: "element",
|
|
1025
|
+
elementId: issue.dataset.elementId
|
|
1026
|
+
});
|
|
1027
|
+
render();
|
|
1028
|
+
};
|
|
1029
|
+
container.addEventListener("click", click);
|
|
1030
|
+
render();
|
|
1031
|
+
return () => container.removeEventListener("click", click);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
function escapeHtml2(value) {
|
|
1038
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
1039
|
+
}
|
|
534
1040
|
|
|
535
|
-
export { Studio, StudioMount, createLocalStorageRepository };
|
|
1041
|
+
export { RESPONSIVE_VIEWPORTS, Studio, StudioMount, addDefaultResponsiveVariants, appendStoryEdge, createLinterPlugin, createLocalStorageRepository, createPerformanceProfilerPlugin, createResponsiveInspectorPlugin, createStoryEditorPlugin, createTemplateEditorPlugin, createVisualRegressionPlugin, initialParameterValue, parameterInputValue, profileAnimation, replaceStoryNodeDocument };
|
|
536
1042
|
//# sourceMappingURL=index.js.map
|
|
537
1043
|
//# sourceMappingURL=index.js.map
|