@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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { updateCanvas, getDef, subscribe, updateMeta,
|
|
2
|
-
import { activeAppearance, animationDocumentSchema, resolveAsset } from '@kokoa/clotho';
|
|
1
|
+
import { updateCanvas, getDef, subscribe, updateMeta, mountEditorPlugins, setSelection, getSelection, setDef, undo, redo, downloadAnimationJson, importAnimation, markClean, updateSettings, isDirty, isDraft, canUndo, canRedo, uniqueChapterId, getCurrentTime, addChapter, uniqueElementId, registerExternalAsset, addElement, registerDataUriAsset, listAnimations, setDraft, saveAnimation, deleteAnimation, duplicateAnimation, createAnimation, getSelectedElementIds, deleteElement, deleteChapter, deleteEffect, updateChapter, isGroup, ungroupElement, groupElements, moveElementToEnd, moveElementToFront, reorderElement, loadAnimation, getCurrentSnapshot, setElementValueAtTime, findContainingGroup, isElementSelected, updateElementBase, createLayout, detachFromLayout, addCheckpoint, uniqueCheckpointId, deleteCheckpoint, addCameraFocus, updateCameraFocus, deleteCameraFocus, setCameraKeyframe, removeCameraKeyframe, removeCameraTrack, setCurrentTime, clearCamera, uniqueEffectId, addEffect, removeAppearance, removeTrack, removeTrackKeyframe, setTrackKeyframe, addAppearance, beginTransient, endTransient, getCamera, jumpBack, jumpForward, getHistory, promoteDraftToSaved, toggleSelectionFor, cameraControlAt, moveGroupBy, placeholderImageUrl, groupBbox, hasCamera, layoutIdsFor, findLayoutCollisions, updateLocales, updateData, updateResponsive, updateDuration, updateCheckpoint, setCameraStrokeScaling, updateAppearance, updateEffect, moveCameraKeyframe, childIdsOf } from './chunk-N47HF4VB.js';
|
|
2
|
+
import { activeAppearance, animationDocumentSchema, bindablePropertiesFor, computeCamera, resolveAsset, compileDataBindings } from '@kokoa/clotho';
|
|
3
3
|
|
|
4
4
|
// src/legacy/grid.ts
|
|
5
5
|
var STORAGE_KEY = "studio.grid";
|
|
@@ -46,6 +46,35 @@ function snapPoint(pt) {
|
|
|
46
46
|
return { x: snap(pt.x), y: snap(pt.y) };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// src/legacy/camera-interactions.ts
|
|
50
|
+
function cameraDragMode(part, control) {
|
|
51
|
+
if (control.kind === "none" || part === "border") return null;
|
|
52
|
+
if (part === "resize") return control.kind === "focus" ? "padding" : "zoom";
|
|
53
|
+
return control.kind === "tracks" ? "pan" : null;
|
|
54
|
+
}
|
|
55
|
+
function centreDistance(point, centreX, centreY) {
|
|
56
|
+
return Math.max(1, Math.hypot(point.x - centreX, point.y - centreY));
|
|
57
|
+
}
|
|
58
|
+
function cameraDragResult(drag, point) {
|
|
59
|
+
if (drag.mode === "pan") {
|
|
60
|
+
return {
|
|
61
|
+
centerX: Math.round(drag.startCenterX - (point.x - drag.startX)),
|
|
62
|
+
centerY: Math.round(drag.startCenterY - (point.y - drag.startY))
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const ratio = centreDistance(point, drag.startCenterX, drag.startCenterY) / drag.startDistance;
|
|
66
|
+
if (drag.mode === "zoom") {
|
|
67
|
+
const zoom = Math.max(0.05, Math.min(20, drag.startZoom / ratio));
|
|
68
|
+
return { zoom: Number(zoom.toFixed(3)) };
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
padding: Math.max(
|
|
72
|
+
0,
|
|
73
|
+
Math.round(drag.startPadding * ratio + (ratio - 1) * 40)
|
|
74
|
+
)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
49
78
|
// src/legacy/canvas-utils.ts
|
|
50
79
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
51
80
|
function escapeXml(s) {
|
|
@@ -646,6 +675,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
|
|
|
646
675
|
rotation: 0,
|
|
647
676
|
appearances: [],
|
|
648
677
|
tracks: [],
|
|
678
|
+
bindings: [],
|
|
649
679
|
x: cx - 60,
|
|
650
680
|
y: cy - 30,
|
|
651
681
|
width: 120,
|
|
@@ -665,6 +695,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
|
|
|
665
695
|
rotation: 0,
|
|
666
696
|
appearances: [],
|
|
667
697
|
tracks: [],
|
|
698
|
+
bindings: [],
|
|
668
699
|
cx,
|
|
669
700
|
cy,
|
|
670
701
|
r: 36,
|
|
@@ -682,6 +713,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
|
|
|
682
713
|
rotation: 0,
|
|
683
714
|
appearances: [],
|
|
684
715
|
tracks: [],
|
|
716
|
+
bindings: [],
|
|
685
717
|
x1: cx - 80,
|
|
686
718
|
y1: cy,
|
|
687
719
|
x2: cx + 80,
|
|
@@ -698,6 +730,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
|
|
|
698
730
|
rotation: 0,
|
|
699
731
|
appearances: [],
|
|
700
732
|
tracks: [],
|
|
733
|
+
bindings: [],
|
|
701
734
|
x1: cx - 100,
|
|
702
735
|
y1: cy,
|
|
703
736
|
x2: cx + 100,
|
|
@@ -718,10 +751,12 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
|
|
|
718
751
|
rotation: 0,
|
|
719
752
|
appearances: [],
|
|
720
753
|
tracks: [],
|
|
754
|
+
bindings: [],
|
|
721
755
|
x: cx,
|
|
722
756
|
y: cy,
|
|
723
757
|
content: id,
|
|
724
758
|
translations: {},
|
|
759
|
+
references: {},
|
|
725
760
|
fontSize: 18,
|
|
726
761
|
fontWeight: 400,
|
|
727
762
|
color: "#18181b",
|
|
@@ -734,6 +769,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
|
|
|
734
769
|
rotation: 0,
|
|
735
770
|
appearances: [],
|
|
736
771
|
tracks: [],
|
|
772
|
+
bindings: [],
|
|
737
773
|
x: cx - 50,
|
|
738
774
|
y: cy - 50,
|
|
739
775
|
width: 100,
|
|
@@ -749,6 +785,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
|
|
|
749
785
|
rotation: 0,
|
|
750
786
|
appearances: [],
|
|
751
787
|
tracks: [],
|
|
788
|
+
bindings: [],
|
|
752
789
|
x: cx - 40,
|
|
753
790
|
y: cy - 40,
|
|
754
791
|
d: "M 0 0 L 80 0 L 40 80 Z",
|
|
@@ -770,6 +807,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
|
|
|
770
807
|
rotation: 0,
|
|
771
808
|
appearances: [],
|
|
772
809
|
tracks: [],
|
|
810
|
+
bindings: [],
|
|
773
811
|
points: pts,
|
|
774
812
|
fill: "#a5b4fc",
|
|
775
813
|
stroke: "#6366f1",
|
|
@@ -1045,8 +1083,6 @@ function renderSelectionOutline(canvasEl2, elId, snap2, byId) {
|
|
|
1045
1083
|
rect.classList.add("element-selected-outline");
|
|
1046
1084
|
return rect;
|
|
1047
1085
|
}
|
|
1048
|
-
|
|
1049
|
-
// src/legacy/canvas-preview.ts
|
|
1050
1086
|
var previewRoot = null;
|
|
1051
1087
|
var previewHandle = null;
|
|
1052
1088
|
var unsubscribePreview = null;
|
|
@@ -1075,7 +1111,7 @@ function showPreview(canvasEl2, def, options) {
|
|
|
1075
1111
|
previewRoot.style.cssText = `position:absolute;left:0;top:0;width:${def.canvas.width}px;height:${def.canvas.height}px;background:${bg};z-index:2;overflow:hidden;`;
|
|
1076
1112
|
parent.style.position = "relative";
|
|
1077
1113
|
parent.appendChild(previewRoot);
|
|
1078
|
-
void mountPreview(previewRoot, def, options);
|
|
1114
|
+
void mountPreview(previewRoot, compileDataBindings(def).document, options);
|
|
1079
1115
|
}
|
|
1080
1116
|
function hidePreview(canvasEl2) {
|
|
1081
1117
|
if (canvasEl2) canvasEl2.style.visibility = "";
|
|
@@ -1147,11 +1183,29 @@ var endpointDragState = null;
|
|
|
1147
1183
|
var vertexDragState = null;
|
|
1148
1184
|
var resizeState = null;
|
|
1149
1185
|
var marqueeState = null;
|
|
1186
|
+
var cameraDragState = null;
|
|
1150
1187
|
var marqueeJustFinished = false;
|
|
1151
1188
|
var toolJustFinished = false;
|
|
1152
1189
|
var canvasEl = null;
|
|
1153
1190
|
var hoveredElementId = null;
|
|
1154
1191
|
var canvasZoom = 1;
|
|
1192
|
+
var showCameraFrame = true;
|
|
1193
|
+
var CAMERA_FRAME_KEY = "studio.canvas.cameraFrame";
|
|
1194
|
+
try {
|
|
1195
|
+
showCameraFrame = localStorage.getItem(CAMERA_FRAME_KEY) !== "off";
|
|
1196
|
+
} catch {
|
|
1197
|
+
}
|
|
1198
|
+
function isCameraFrameVisible() {
|
|
1199
|
+
return showCameraFrame;
|
|
1200
|
+
}
|
|
1201
|
+
function setCameraFrameVisible(value) {
|
|
1202
|
+
showCameraFrame = value;
|
|
1203
|
+
try {
|
|
1204
|
+
localStorage.setItem(CAMERA_FRAME_KEY, value ? "on" : "off");
|
|
1205
|
+
} catch {
|
|
1206
|
+
}
|
|
1207
|
+
requestCanvasRender();
|
|
1208
|
+
}
|
|
1155
1209
|
var toolDrawState = null;
|
|
1156
1210
|
var elementDrawState = null;
|
|
1157
1211
|
var canvasRenderFrame = null;
|
|
@@ -1284,7 +1338,7 @@ function onCanvasClick(e) {
|
|
|
1284
1338
|
return;
|
|
1285
1339
|
}
|
|
1286
1340
|
if (target?.closest(
|
|
1287
|
-
"[data-rotate-handle], [data-anchor-handle], [data-resize-handle]"
|
|
1341
|
+
"[data-rotate-handle], [data-anchor-handle], [data-resize-handle], [data-camera-frame]"
|
|
1288
1342
|
))
|
|
1289
1343
|
return;
|
|
1290
1344
|
const id = findElementId(e.target);
|
|
@@ -1297,6 +1351,8 @@ function onCanvasClick(e) {
|
|
|
1297
1351
|
function onMouseDown(e) {
|
|
1298
1352
|
if (e.button !== 0) return;
|
|
1299
1353
|
const target = e.target;
|
|
1354
|
+
const cameraPart = target?.closest("[data-camera-frame]");
|
|
1355
|
+
if (cameraPart && beginCameraDrag(e, cameraPart)) return;
|
|
1300
1356
|
const def = getDef();
|
|
1301
1357
|
if (!def) return;
|
|
1302
1358
|
const activeTool2 = getActiveTool();
|
|
@@ -1561,6 +1617,11 @@ function collectDragExtras(anchorId, def, snap2) {
|
|
|
1561
1617
|
return extras;
|
|
1562
1618
|
}
|
|
1563
1619
|
function onMouseMove(e) {
|
|
1620
|
+
if (cameraDragState) {
|
|
1621
|
+
e.preventDefault();
|
|
1622
|
+
moveCameraDrag(e);
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1564
1625
|
if (pathDraftState && getActiveTool() === "path") {
|
|
1565
1626
|
const point = svgPoint(e.clientX, e.clientY);
|
|
1566
1627
|
if (point) {
|
|
@@ -1761,6 +1822,11 @@ function finishPathDraft() {
|
|
|
1761
1822
|
render2();
|
|
1762
1823
|
}
|
|
1763
1824
|
function onMouseUp(e) {
|
|
1825
|
+
if (cameraDragState) {
|
|
1826
|
+
cameraDragState = null;
|
|
1827
|
+
endTransient();
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1764
1830
|
if (elementDrawState) {
|
|
1765
1831
|
const draw = elementDrawState;
|
|
1766
1832
|
const point = svgPoint(e.clientX, e.clientY);
|
|
@@ -1824,6 +1890,7 @@ function onMouseUp(e) {
|
|
|
1824
1890
|
rotation: 0,
|
|
1825
1891
|
appearances: [],
|
|
1826
1892
|
tracks: [],
|
|
1893
|
+
bindings: [],
|
|
1827
1894
|
fromId: connectState.fromId,
|
|
1828
1895
|
toId: elemId,
|
|
1829
1896
|
fromAnchor: connectState.fromAnchor,
|
|
@@ -2168,6 +2235,8 @@ function render2() {
|
|
|
2168
2235
|
canvasEl.appendChild(g);
|
|
2169
2236
|
}
|
|
2170
2237
|
}
|
|
2238
|
+
const cameraFrame = renderCameraFrame(def);
|
|
2239
|
+
if (cameraFrame) canvasEl.appendChild(cameraFrame);
|
|
2171
2240
|
const selection = getSelection();
|
|
2172
2241
|
if (selection.kind === "element") {
|
|
2173
2242
|
const selEl = elementsById.get(selection.elementId);
|
|
@@ -2378,6 +2447,141 @@ function renderGroupOutline(groupId) {
|
|
|
2378
2447
|
`;
|
|
2379
2448
|
return g;
|
|
2380
2449
|
}
|
|
2450
|
+
function renderCameraFrame(def) {
|
|
2451
|
+
if (!showCameraFrame) return null;
|
|
2452
|
+
const view = computeCamera(def, getCurrentTime());
|
|
2453
|
+
if (!view) return null;
|
|
2454
|
+
const { width, height } = def.canvas;
|
|
2455
|
+
const selected = getSelection().kind === "camera";
|
|
2456
|
+
const control = cameraControlAt(getCurrentTime());
|
|
2457
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
2458
|
+
g.setAttribute(
|
|
2459
|
+
"class",
|
|
2460
|
+
`studio-camera-frame${selected ? " is-selected" : ""}`
|
|
2461
|
+
);
|
|
2462
|
+
g.setAttribute("pointer-events", "none");
|
|
2463
|
+
const scrim = document.createElementNS(SVG_NS, "path");
|
|
2464
|
+
scrim.setAttribute(
|
|
2465
|
+
"d",
|
|
2466
|
+
`M0 0H${width}V${height}H0Z M${view.x} ${view.y}H${view.x + view.width}V${view.y + view.height}H${view.x}Z`
|
|
2467
|
+
);
|
|
2468
|
+
scrim.setAttribute("fill-rule", "evenodd");
|
|
2469
|
+
scrim.setAttribute("class", "studio-camera-frame-scrim");
|
|
2470
|
+
g.appendChild(scrim);
|
|
2471
|
+
const rect = document.createElementNS(SVG_NS, "rect");
|
|
2472
|
+
rect.setAttribute("x", String(view.x));
|
|
2473
|
+
rect.setAttribute("y", String(view.y));
|
|
2474
|
+
rect.setAttribute("width", String(view.width));
|
|
2475
|
+
rect.setAttribute("height", String(view.height));
|
|
2476
|
+
rect.setAttribute("class", "studio-camera-frame-rect");
|
|
2477
|
+
rect.setAttribute("vector-effect", "non-scaling-stroke");
|
|
2478
|
+
rect.setAttribute("data-camera-frame", "border");
|
|
2479
|
+
rect.setAttribute("pointer-events", "stroke");
|
|
2480
|
+
g.appendChild(rect);
|
|
2481
|
+
if (selected && control.kind !== "none") {
|
|
2482
|
+
g.appendChild(cameraHandles(view, control));
|
|
2483
|
+
}
|
|
2484
|
+
const label = document.createElementNS(SVG_NS, "text");
|
|
2485
|
+
label.setAttribute("x", String(view.x + 6));
|
|
2486
|
+
label.setAttribute("y", String(view.y + 16));
|
|
2487
|
+
label.setAttribute("class", "studio-camera-frame-label");
|
|
2488
|
+
label.textContent = selected ? `\u{1F3A5} ${view.zoom.toFixed(2)}\xD7 \xB7 ${Math.round(view.centerX)}, ${Math.round(view.centerY)} \xB7 ${control.kind === "focus" ? `focus #${control.index + 1} \u2014 \uBAA8\uC11C\uB9AC\uB97C \uB04C\uBA74 padding` : "\uB04C\uC5B4\uC11C \uC774\uB3D9 \xB7 \uBAA8\uC11C\uB9AC\uB85C zoom"}` : `\u{1F3A5} ${view.zoom.toFixed(2)}\xD7 \xB7 ${Math.round(view.centerX)}, ${Math.round(view.centerY)}`;
|
|
2489
|
+
g.appendChild(label);
|
|
2490
|
+
if (view.issues.length > 0) {
|
|
2491
|
+
const warning = document.createElementNS(SVG_NS, "text");
|
|
2492
|
+
warning.setAttribute("x", String(view.x + 6));
|
|
2493
|
+
warning.setAttribute("y", String(view.y + 32));
|
|
2494
|
+
warning.setAttribute("class", "studio-camera-frame-warning");
|
|
2495
|
+
warning.textContent = `\u26A0 ${view.issues[0].message}`;
|
|
2496
|
+
g.appendChild(warning);
|
|
2497
|
+
}
|
|
2498
|
+
return g;
|
|
2499
|
+
}
|
|
2500
|
+
function beginCameraDrag(e, part) {
|
|
2501
|
+
const def = getDef();
|
|
2502
|
+
if (!def) return false;
|
|
2503
|
+
const time = getCurrentTime();
|
|
2504
|
+
const view = computeCamera(def, time);
|
|
2505
|
+
if (!view) return false;
|
|
2506
|
+
const wasSelected = getSelection().kind === "camera";
|
|
2507
|
+
if (!wasSelected) setSelection({ kind: "camera" });
|
|
2508
|
+
e.preventDefault();
|
|
2509
|
+
e.stopPropagation();
|
|
2510
|
+
const kind = part.dataset.cameraFrame;
|
|
2511
|
+
if (kind === "border" || !wasSelected) return true;
|
|
2512
|
+
const control = cameraControlAt(time);
|
|
2513
|
+
const mode = cameraDragMode(kind, control);
|
|
2514
|
+
if (!mode) return true;
|
|
2515
|
+
const point = svgPoint(e.clientX, e.clientY);
|
|
2516
|
+
if (!point) return true;
|
|
2517
|
+
const focusIndex = control.kind === "focus" ? control.index : -1;
|
|
2518
|
+
const padding = focusIndex >= 0 ? getCamera().focus[focusIndex]?.padding ?? 24 : 0;
|
|
2519
|
+
cameraDragState = {
|
|
2520
|
+
mode,
|
|
2521
|
+
focusIndex,
|
|
2522
|
+
time,
|
|
2523
|
+
startX: point.x,
|
|
2524
|
+
startY: point.y,
|
|
2525
|
+
startCenterX: view.centerX,
|
|
2526
|
+
startCenterY: view.centerY,
|
|
2527
|
+
startZoom: view.zoom,
|
|
2528
|
+
startPadding: padding,
|
|
2529
|
+
startDistance: centreDistance(point, view.centerX, view.centerY)
|
|
2530
|
+
};
|
|
2531
|
+
beginTransient("\uCE74\uBA54\uB77C \uC870\uC815", "camera");
|
|
2532
|
+
return true;
|
|
2533
|
+
}
|
|
2534
|
+
function moveCameraDrag(e) {
|
|
2535
|
+
const drag = cameraDragState;
|
|
2536
|
+
if (!drag) return;
|
|
2537
|
+
const point = svgPoint(e.clientX, e.clientY);
|
|
2538
|
+
if (!point) return;
|
|
2539
|
+
const next = cameraDragResult(drag, point);
|
|
2540
|
+
if (next.centerX !== void 0) {
|
|
2541
|
+
setCameraKeyframe("x", drag.time, next.centerX);
|
|
2542
|
+
}
|
|
2543
|
+
if (next.centerY !== void 0) {
|
|
2544
|
+
setCameraKeyframe("y", drag.time, next.centerY);
|
|
2545
|
+
}
|
|
2546
|
+
if (next.zoom !== void 0) setCameraKeyframe("zoom", drag.time, next.zoom);
|
|
2547
|
+
if (next.padding !== void 0) {
|
|
2548
|
+
updateCameraFocus(drag.focusIndex, { padding: next.padding });
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
function cameraHandles(view, control) {
|
|
2552
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
2553
|
+
if (control.kind === "tracks") {
|
|
2554
|
+
const body = document.createElementNS(SVG_NS, "rect");
|
|
2555
|
+
body.setAttribute("x", String(view.x));
|
|
2556
|
+
body.setAttribute("y", String(view.y));
|
|
2557
|
+
body.setAttribute("width", String(view.width));
|
|
2558
|
+
body.setAttribute("height", String(view.height));
|
|
2559
|
+
body.setAttribute("class", "studio-camera-frame-body");
|
|
2560
|
+
body.setAttribute("data-camera-frame", "pan");
|
|
2561
|
+
body.setAttribute("pointer-events", "fill");
|
|
2562
|
+
g.appendChild(body);
|
|
2563
|
+
}
|
|
2564
|
+
const corners = [
|
|
2565
|
+
["nw", view.x, view.y],
|
|
2566
|
+
["ne", view.x + view.width, view.y],
|
|
2567
|
+
["sw", view.x, view.y + view.height],
|
|
2568
|
+
["se", view.x + view.width, view.y + view.height]
|
|
2569
|
+
];
|
|
2570
|
+
const size = 9 / canvasZoom;
|
|
2571
|
+
for (const [corner, cx, cy] of corners) {
|
|
2572
|
+
const handle = document.createElementNS(SVG_NS, "rect");
|
|
2573
|
+
handle.setAttribute("x", String(cx - size / 2));
|
|
2574
|
+
handle.setAttribute("y", String(cy - size / 2));
|
|
2575
|
+
handle.setAttribute("width", String(size));
|
|
2576
|
+
handle.setAttribute("height", String(size));
|
|
2577
|
+
handle.setAttribute("class", "studio-camera-frame-handle");
|
|
2578
|
+
handle.setAttribute("data-camera-frame", "resize");
|
|
2579
|
+
handle.setAttribute("data-camera-corner", corner);
|
|
2580
|
+
handle.setAttribute("pointer-events", "all");
|
|
2581
|
+
g.appendChild(handle);
|
|
2582
|
+
}
|
|
2583
|
+
return g;
|
|
2584
|
+
}
|
|
2381
2585
|
function makeG(elementId, rotation, cx, cy) {
|
|
2382
2586
|
const g = document.createElementNS(SVG_NS, "g");
|
|
2383
2587
|
g.setAttribute("data-elem-id", elementId);
|
|
@@ -2708,6 +2912,41 @@ function parseLocales(value) {
|
|
|
2708
2912
|
return true;
|
|
2709
2913
|
});
|
|
2710
2914
|
}
|
|
2915
|
+
function annotationTokens(...values) {
|
|
2916
|
+
const tokens = values.flatMap(
|
|
2917
|
+
(value) => [...value.matchAll(/\{([a-z][a-z0-9_-]*)\}/g)].map((match) => match[1])
|
|
2918
|
+
);
|
|
2919
|
+
return [...new Set(tokens)];
|
|
2920
|
+
}
|
|
2921
|
+
function annotationReferenceFields(keyPrefix, values, references = {}) {
|
|
2922
|
+
const tokens = annotationTokens(...values);
|
|
2923
|
+
if (tokens.length === 0) {
|
|
2924
|
+
return [
|
|
2925
|
+
'<p class="studio-props-empty studio-annotation-hint">\uBB38\uAD6C\uC5D0 {token}\uC744 \uC785\uB825\uD558\uBA74 \uC7A5\uBA74 \uC694\uC18C\uC640 \uC5F0\uACB0\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.</p>'
|
|
2926
|
+
];
|
|
2927
|
+
}
|
|
2928
|
+
return [
|
|
2929
|
+
'<p class="studio-props-empty studio-annotation-hint">\uB300\uC0C1 element id\uB97C \uC27C\uD45C\uB85C \uAD6C\uBD84\uD558\uC138\uC694. \uC5EC\uB7EC \uC694\uC18C\uB97C \uD568\uAED8 \uAC15\uC870\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.</p>',
|
|
2930
|
+
...tokens.map((token) => {
|
|
2931
|
+
const target = references[token];
|
|
2932
|
+
const value = typeof target === "string" ? target : target?.join(", ") ?? "";
|
|
2933
|
+
return textField(
|
|
2934
|
+
`{${token}} \uB300\uC0C1`,
|
|
2935
|
+
`${keyPrefix}.reference.${token}`,
|
|
2936
|
+
value
|
|
2937
|
+
);
|
|
2938
|
+
})
|
|
2939
|
+
];
|
|
2940
|
+
}
|
|
2941
|
+
function parseReferenceTargets(value) {
|
|
2942
|
+
const targets = [
|
|
2943
|
+
...new Set(
|
|
2944
|
+
value.split(",").map((id) => id.trim()).filter(Boolean)
|
|
2945
|
+
)
|
|
2946
|
+
];
|
|
2947
|
+
if (targets.length === 0) return void 0;
|
|
2948
|
+
return targets.length === 1 ? targets[0] : targets;
|
|
2949
|
+
}
|
|
2711
2950
|
function initProperties(root) {
|
|
2712
2951
|
panelEl = root;
|
|
2713
2952
|
subscribe(render3);
|
|
@@ -2719,8 +2958,23 @@ function initProperties(root) {
|
|
|
2719
2958
|
root.addEventListener("compositionend", onCompositionEnd);
|
|
2720
2959
|
root.addEventListener("toggle", onSectionToggle, true);
|
|
2721
2960
|
root.addEventListener("wheel", onNumberWheel, { passive: false });
|
|
2961
|
+
root.addEventListener("pointerdown", onSliderGrab);
|
|
2962
|
+
root.addEventListener("pointerup", onSliderRelease);
|
|
2963
|
+
root.addEventListener("pointercancel", onSliderRelease);
|
|
2722
2964
|
render3();
|
|
2723
2965
|
}
|
|
2966
|
+
var slidingKey = null;
|
|
2967
|
+
function onSliderGrab(e) {
|
|
2968
|
+
const target = e.target;
|
|
2969
|
+
if (target.type !== "range" || !target.dataset.propKey) return;
|
|
2970
|
+
slidingKey = target.dataset.propKey;
|
|
2971
|
+
beginTransient(`\uC870\uC815: ${slidingKey}`, "camera");
|
|
2972
|
+
}
|
|
2973
|
+
function onSliderRelease() {
|
|
2974
|
+
if (slidingKey === null) return;
|
|
2975
|
+
slidingKey = null;
|
|
2976
|
+
endTransient();
|
|
2977
|
+
}
|
|
2724
2978
|
function onCompositionStart() {
|
|
2725
2979
|
isComposingFallback = true;
|
|
2726
2980
|
}
|
|
@@ -2779,6 +3033,9 @@ function textField(label, key, value) {
|
|
|
2779
3033
|
<input type="text" data-prop-key="${escapeHtml2(key)}" value="${escapeHtml2(value ?? "")}" />
|
|
2780
3034
|
</label>`;
|
|
2781
3035
|
}
|
|
3036
|
+
function textareaField(label, key, value) {
|
|
3037
|
+
return `<label class="studio-field"><span>${escapeHtml2(label)}</span><textarea rows="7" spellcheck="false" data-prop-key="${escapeHtml2(key)}">${escapeHtml2(value)}</textarea></label>`;
|
|
3038
|
+
}
|
|
2782
3039
|
function numberField(label, key, value, step = 1) {
|
|
2783
3040
|
return `<label class="studio-field">
|
|
2784
3041
|
<span>${escapeHtml2(label)}</span>
|
|
@@ -2833,7 +3090,18 @@ function renderInner() {
|
|
|
2833
3090
|
"canvas.background",
|
|
2834
3091
|
"canvas.background",
|
|
2835
3092
|
def.canvas.background
|
|
2836
|
-
)
|
|
3093
|
+
),
|
|
3094
|
+
textareaField(
|
|
3095
|
+
"\uC0D8\uD50C \uB370\uC774\uD130 (JSON)",
|
|
3096
|
+
"meta.data",
|
|
3097
|
+
JSON.stringify(def.data, null, 2)
|
|
3098
|
+
),
|
|
3099
|
+
textareaField(
|
|
3100
|
+
"Responsive variants (JSON)",
|
|
3101
|
+
"meta.responsive",
|
|
3102
|
+
JSON.stringify(def.responsive ?? [], null, 2)
|
|
3103
|
+
),
|
|
3104
|
+
`<p class="studio-props-empty">JSON Pointer\uB85C \uC694\uC18C \uC18D\uC131\uC5D0 \uC5F0\uACB0\uD569\uB2C8\uB2E4. \uC774 \uB370\uC774\uD130\uB294 \uBBF8\uB9AC\uBCF4\uAE30\uC640 \uB0B4\uBCF4\uB0B4\uAE30\uC5D0 \uD568\uAED8 \uC800\uC7A5\uB429\uB2C8\uB2E4.</p>`
|
|
2837
3105
|
].join("");
|
|
2838
3106
|
const settingsHeader = `<span class="studio-props-header-title">\uC124\uC815</span>`;
|
|
2839
3107
|
const settingsBody = [
|
|
@@ -2861,10 +3129,61 @@ function renderInner() {
|
|
|
2861
3129
|
]
|
|
2862
3130
|
)
|
|
2863
3131
|
].join("");
|
|
3132
|
+
const checkpoints = def.checkpoints.map((checkpoint, index) => {
|
|
3133
|
+
const specific = checkpoint.interaction === "choice" ? textField(
|
|
3134
|
+
"\uC120\uD0DD\uC9C0 (value:label, \uC27C\uD45C \uAD6C\uBD84)",
|
|
3135
|
+
`checkpoint.${index}.options`,
|
|
3136
|
+
checkpoint.options.map(({ value, label }) => `${value}:${label}`).join(", ")
|
|
3137
|
+
) : checkpoint.interaction === "select-element" ? textField(
|
|
3138
|
+
"\uC120\uD0DD \uAC00\uB2A5\uD55C element id",
|
|
3139
|
+
`checkpoint.${index}.elementIds`,
|
|
3140
|
+
checkpoint.elementIds.join(", ")
|
|
3141
|
+
) : checkpoint.interaction === "number-input" ? [
|
|
3142
|
+
numberField(
|
|
3143
|
+
"\uCD5C\uC19F\uAC12",
|
|
3144
|
+
`checkpoint.${index}.min`,
|
|
3145
|
+
checkpoint.min
|
|
3146
|
+
),
|
|
3147
|
+
numberField(
|
|
3148
|
+
"\uCD5C\uB313\uAC12",
|
|
3149
|
+
`checkpoint.${index}.max`,
|
|
3150
|
+
checkpoint.max
|
|
3151
|
+
),
|
|
3152
|
+
numberField(
|
|
3153
|
+
"\uAC04\uACA9",
|
|
3154
|
+
`checkpoint.${index}.step`,
|
|
3155
|
+
checkpoint.step
|
|
3156
|
+
)
|
|
3157
|
+
].join("") : "";
|
|
3158
|
+
const predicate = "predicate" in checkpoint && checkpoint.predicate?.type === "equals" ? textField(
|
|
3159
|
+
"\uC815\uB2F5 (equals)",
|
|
3160
|
+
`checkpoint.${index}.answer`,
|
|
3161
|
+
String(checkpoint.predicate.value)
|
|
3162
|
+
) : "";
|
|
3163
|
+
return `<div class="studio-checkpoint-card" data-checkpoint-id="${escapeHtml2(checkpoint.id)}">
|
|
3164
|
+
<div class="studio-props-header"><span class="studio-props-header-title">${escapeHtml2(checkpoint.id)}</span><button type="button" class="studio-btn studio-btn-danger" data-delete-checkpoint="${escapeHtml2(checkpoint.id)}">\uC0AD\uC81C</button></div>
|
|
3165
|
+
${numberField("time (ms)", `checkpoint.${index}.time`, checkpoint.time, 50)}
|
|
3166
|
+
${textField("\uC9C8\uBB38", `checkpoint.${index}.prompt`, checkpoint.prompt)}
|
|
3167
|
+
${selectField(
|
|
3168
|
+
"\uC0C1\uD638\uC791\uC6A9",
|
|
3169
|
+
`checkpoint.${index}.interaction`,
|
|
3170
|
+
checkpoint.interaction,
|
|
3171
|
+
[
|
|
3172
|
+
{ value: "continue", label: "\uACC4\uC18D" },
|
|
3173
|
+
{ value: "choice", label: "\uC120\uD0DD\uC9C0" },
|
|
3174
|
+
{ value: "select-element", label: "\uC694\uC18C \uC120\uD0DD" },
|
|
3175
|
+
{ value: "number-input", label: "\uC22B\uC790 \uC785\uB825" }
|
|
3176
|
+
]
|
|
3177
|
+
)}
|
|
3178
|
+
${checkboxField("\uC751\uB2F5 \uD544\uC218", `checkpoint.${index}.required`, checkpoint.required)}
|
|
3179
|
+
${specific}${predicate}
|
|
3180
|
+
</div>`;
|
|
3181
|
+
}).join("");
|
|
2864
3182
|
panelEl.innerHTML = `
|
|
2865
3183
|
${timeHint}
|
|
2866
3184
|
${section("meta", metaHeader, metaBody)}
|
|
2867
3185
|
${section("settings", settingsHeader, settingsBody)}
|
|
3186
|
+
${section("checkpoints", `<span class="studio-props-header-title">Checkpoint (${def.checkpoints.length})</span>`, `${checkpoints}<button type="button" class="studio-btn" data-add-checkpoint>\uFF0B \uD604\uC7AC \uC2DC\uAC04\uC5D0 checkpoint \uCD94\uAC00</button>`)}
|
|
2868
3187
|
<div class="studio-props-header" style="margin-top:0.6rem"><span class="studio-props-header-title">\uBAA9\uCC28 (${def.chapters.length})</span></div>
|
|
2869
3188
|
<button type="button" class="studio-btn" data-add-chapter>\uFF0B \uD604\uC7AC \uC2DC\uAC04\uC5D0 chapter \uCD94\uAC00</button>
|
|
2870
3189
|
<div class="studio-props-header" style="margin-top:0.6rem"><span class="studio-props-header-title">\uD6A8\uACFC (${def.effects.length})</span></div>
|
|
@@ -2876,12 +3195,18 @@ function renderInner() {
|
|
|
2876
3195
|
</select>
|
|
2877
3196
|
<button type="button" class="studio-btn" data-add-effect>\uFF0B \uD6A8\uACFC</button>
|
|
2878
3197
|
</div>
|
|
3198
|
+
<div class="studio-props-header" style="margin-top:0.6rem"><span class="studio-props-header-title">\uCE74\uBA54\uB77C</span><span class="studio-props-header-type">${hasCamera() ? `focus ${getCamera().focus.length} \xB7 track ${getCamera().tracks.length}` : "\uC5C6\uC74C"}</span></div>
|
|
3199
|
+
<button type="button" class="studio-btn" data-open-camera>\u{1F3A5} \uCE74\uBA54\uB77C \uD3B8\uC9D1</button>
|
|
2879
3200
|
`;
|
|
2880
3201
|
return;
|
|
2881
3202
|
}
|
|
2882
3203
|
if (sel.kind === "elements") {
|
|
2883
3204
|
const alignBtn = (kind, label, title) => `<button type="button" class="studio-btn studio-align-btn" data-align="${kind}" title="${escapeHtml2(title)}" aria-label="${escapeHtml2(title)}">${label}</button>`;
|
|
2884
3205
|
const distributeDisabled = sel.elementIds.length < 3 ? "disabled" : "";
|
|
3206
|
+
const layoutIds = layoutIdsFor(sel.elementIds);
|
|
3207
|
+
const collisions = findLayoutCollisions(def).filter(
|
|
3208
|
+
({ firstId, secondId }) => sel.elementIds.includes(firstId) || sel.elementIds.includes(secondId)
|
|
3209
|
+
);
|
|
2885
3210
|
panelEl.innerHTML = `
|
|
2886
3211
|
${timeHint}
|
|
2887
3212
|
<div class="studio-props-header"><span class="studio-props-header-title">\uB2E4\uC911 \uC120\uD0DD</span><span class="studio-props-header-type">${sel.elementIds.length} elements</span></div>
|
|
@@ -2905,6 +3230,16 @@ function renderInner() {
|
|
|
2905
3230
|
<button type="button" class="studio-btn studio-align-btn" data-distribute="vertical" title="\uC138\uB85C \uADE0\uB4F1 \uBD84\uD3EC" ${distributeDisabled}>\u2195</button>
|
|
2906
3231
|
</div>
|
|
2907
3232
|
</div>
|
|
3233
|
+
<div class="studio-align-section studio-layout-section">
|
|
3234
|
+
<div class="studio-align-title">Constraint Layout</div>
|
|
3235
|
+
<div class="studio-align-row">
|
|
3236
|
+
<button type="button" class="studio-btn" data-create-layout="row">\uAC00\uB85C</button>
|
|
3237
|
+
<button type="button" class="studio-btn" data-create-layout="column">\uC138\uB85C</button>
|
|
3238
|
+
<button type="button" class="studio-btn" data-create-layout="grid">\uACA9\uC790</button>
|
|
3239
|
+
</div>
|
|
3240
|
+
${layoutIds.length > 0 ? `<p class="studio-layout-status">\uC801\uC6A9 \uC911: ${layoutIds.map(escapeHtml2).join(", ")}</p><button type="button" class="studio-btn" data-detach-layout>\uD604\uC7AC \uC88C\uD45C\uB85C \uACE0\uC815</button>` : '<p class="studio-layout-status">\uC120\uD0DD\uD55C \uC694\uC18C\uC5D0 \uC801\uC6A9\uB41C layout\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.</p>'}
|
|
3241
|
+
${collisions.length > 0 ? `<p class="studio-layout-collision" role="alert">\uACB9\uCE68: ${collisions.map(({ firstId, secondId }) => `${escapeHtml2(firstId)} \u2194 ${escapeHtml2(secondId)}`).join(", ")}</p>` : ""}
|
|
3242
|
+
</div>
|
|
2908
3243
|
<ul style="font-family:var(--font-mono);font-size:0.72rem;color:var(--color-fg-muted);padding-left:1rem;margin:0.6rem 0 0">
|
|
2909
3244
|
${sel.elementIds.map((id) => `<li>${escapeHtml2(id)}</li>`).join("")}
|
|
2910
3245
|
</ul>
|
|
@@ -2920,18 +3255,24 @@ function renderInner() {
|
|
|
2920
3255
|
renderElementForm(def, el);
|
|
2921
3256
|
return;
|
|
2922
3257
|
}
|
|
3258
|
+
if (sel.kind === "camera") {
|
|
3259
|
+
renderCameraForm(def, sel.focusIndex);
|
|
3260
|
+
return;
|
|
3261
|
+
}
|
|
2923
3262
|
if (sel.kind === "chapter") {
|
|
2924
3263
|
const ch = def.chapters.find((c) => c.id === sel.chapterId);
|
|
2925
3264
|
if (!ch) {
|
|
2926
3265
|
setSelection({ kind: "none" });
|
|
2927
3266
|
return;
|
|
2928
3267
|
}
|
|
3268
|
+
const chapterReferences = ch.references;
|
|
2929
3269
|
panelEl.innerHTML = `
|
|
2930
3270
|
${timeHint}
|
|
2931
3271
|
<div class="studio-props-header"><span class="studio-props-header-title">${escapeHtml2(ch.id)}</span><span class="studio-props-header-type">chapter</span></div>
|
|
2932
3272
|
${numberField("time (ms)", "chapter.time", ch.time, 50)}
|
|
2933
3273
|
${textField("label", "chapter.label", ch.label)}
|
|
2934
3274
|
${textField("subtitle", "chapter.subtitle", ch.subtitle)}
|
|
3275
|
+
${annotationReferenceFields("chapter", [ch.label, ch.subtitle], chapterReferences).join("")}
|
|
2935
3276
|
<button type="button" class="studio-btn studio-btn-danger" data-delete-chapter style="margin-top:0.6rem">\u{1F5D1} chapter \uC0AD\uC81C</button>
|
|
2936
3277
|
`;
|
|
2937
3278
|
return;
|
|
@@ -2962,6 +3303,114 @@ function renderInner() {
|
|
|
2962
3303
|
return;
|
|
2963
3304
|
}
|
|
2964
3305
|
}
|
|
3306
|
+
function rangeField(label, key, value, min, max, step, hint) {
|
|
3307
|
+
return `<div class="studio-camera-range">
|
|
3308
|
+
<div class="studio-camera-range-head"><span>${escapeHtml2(label)}</span><span class="studio-camera-range-value">${value}</span></div>
|
|
3309
|
+
<div class="studio-camera-range-row">
|
|
3310
|
+
<input type="range" data-prop-key="${escapeHtml2(key)}" min="${min}" max="${max}" step="${step}" value="${value}" />
|
|
3311
|
+
<input type="number" data-prop-key="${escapeHtml2(key)}" step="${step}" value="${value}" />
|
|
3312
|
+
</div>
|
|
3313
|
+
${hint ? `<p class="studio-camera-hint">${escapeHtml2(hint)}</p>` : ""}
|
|
3314
|
+
</div>`;
|
|
3315
|
+
}
|
|
3316
|
+
function focusTargetPicker(def, index, selected) {
|
|
3317
|
+
const chosen = new Set(selected);
|
|
3318
|
+
const rows = def.elements.map((element) => {
|
|
3319
|
+
const name = friendlyElementLabel(element);
|
|
3320
|
+
const same = name === element.id;
|
|
3321
|
+
return `<label class="studio-camera-target ${chosen.has(element.id) ? "is-on" : ""}">
|
|
3322
|
+
<input type="checkbox" data-camera-target="${index}" value="${escapeHtml2(element.id)}" ${chosen.has(element.id) ? "checked" : ""} />
|
|
3323
|
+
<span class="studio-camera-target-name">${escapeHtml2(name)}</span>
|
|
3324
|
+
${same ? "" : `<span class="studio-camera-target-id">${escapeHtml2(element.id)}</span>`}
|
|
3325
|
+
<span class="studio-camera-target-type">${element.type}</span>
|
|
3326
|
+
</label>`;
|
|
3327
|
+
}).join("");
|
|
3328
|
+
const missing = selected.filter(
|
|
3329
|
+
(id) => !def.elements.some((element) => element.id === id)
|
|
3330
|
+
);
|
|
3331
|
+
return `<div class="studio-camera-targets">
|
|
3332
|
+
${rows || '<p class="studio-props-empty">\uC694\uC18C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</p>'}
|
|
3333
|
+
${missing.length > 0 ? `<p class="studio-camera-issue">\u26A0 \uBB38\uC11C\uC5D0 \uC5C6\uB294 id: ${escapeHtml2(missing.join(", "))}</p>` : ""}
|
|
3334
|
+
</div>`;
|
|
3335
|
+
}
|
|
3336
|
+
function renderCameraForm(def, focusIndex) {
|
|
3337
|
+
if (!panelEl) return;
|
|
3338
|
+
const camera = getCamera();
|
|
3339
|
+
const time = getCurrentTime();
|
|
3340
|
+
const view = computeCamera(def, time);
|
|
3341
|
+
const control = cameraControlAt(time);
|
|
3342
|
+
const scalingOpts = ["scale", "fixed"].map(
|
|
3343
|
+
(value) => `<option value="${value}" ${value === camera.strokeScaling ? "selected" : ""}>${value}${value === "scale" ? " (\uAE30\uBCF8)" : ""}</option>`
|
|
3344
|
+
).join("");
|
|
3345
|
+
const controlNote = control.kind === "focus" ? `focus #${control.index + 1}\uC774(\uAC00) \uC774 \uC2DC\uAC01\uC744 \uACB0\uC815\uD569\uB2C8\uB2E4 \xB7 \uCE94\uBC84\uC2A4 \uBAA8\uC11C\uB9AC\uB97C \uB04C\uBA74 padding` : control.kind === "tracks" ? "track\uC774 \uC774 \uC2DC\uAC01\uC744 \uACB0\uC815\uD569\uB2C8\uB2E4 \xB7 \uCE94\uBC84\uC2A4\uC5D0\uC11C \uB04C\uC5B4 \uC774\uB3D9, \uBAA8\uC11C\uB9AC\uB85C zoom" : "\uCE74\uBA54\uB77C \uC5C6\uC74C";
|
|
3346
|
+
const readout = view ? `<div class="studio-camera-readout">zoom ${view.zoom.toFixed(2)}\xD7 \xB7 center ${Math.round(view.centerX)}, ${Math.round(view.centerY)}
|
|
3347
|
+
<br/><span class="studio-camera-control">${escapeHtml2(controlNote)}</span>${view.issues.length > 0 ? `<br/><span class="studio-camera-issue">\u26A0 ${escapeHtml2(view.issues[0].message)}</span>` : ""}</div>` : '<div class="studio-camera-readout">\uC774 \uBB38\uC11C\uC5D0\uB294 \uCE74\uBA54\uB77C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uC544\uB798\uC5D0\uC11C focus\uB098 track\uC744 \uCD94\uAC00\uD558\uBA74 \uC0DD\uAE41\uB2C8\uB2E4.</div>';
|
|
3348
|
+
const focusRows = camera.focus.map((entry, index) => {
|
|
3349
|
+
const open = index === focusIndex;
|
|
3350
|
+
const targets = entry.elementIds.join(", ");
|
|
3351
|
+
const preset = (value, label) => `<button type="button" class="studio-chip ${entry.duration === value ? "is-on" : ""}" data-camera-duration="${index}:${value}">${label}</button>`;
|
|
3352
|
+
return `
|
|
3353
|
+
<details class="studio-camera-focus" ${open ? "open" : ""}>
|
|
3354
|
+
<summary>#${index + 1} \xB7 ${entry.time}ms \u2192 ${escapeHtml2(targets)}</summary>
|
|
3355
|
+
<div class="studio-camera-focus-actions">
|
|
3356
|
+
<button type="button" class="studio-btn" data-camera-seek="${entry.time}" title="\uC774 focus\uAC00 \uC2DC\uC791\uD558\uB294 \uC2DC\uAC01\uC73C\uB85C \uC774\uB3D9">\u23F1 ${entry.time}ms\uB85C \uC774\uB3D9</button>
|
|
3357
|
+
<button type="button" class="studio-btn" data-camera-focus-now="${index}" title="\uD604\uC7AC \uC7AC\uC0DD \uC704\uCE58\uB97C \uC774 focus\uC758 \uC2DC\uC791 \uC2DC\uAC01\uC73C\uB85C">\uD604\uC7AC \uC2DC\uAC01\uC73C\uB85C</button>
|
|
3358
|
+
</div>
|
|
3359
|
+
${numberField("time (ms)", `camera.focus.${index}.time`, entry.time, 50)}
|
|
3360
|
+
<div class="studio-camera-range">
|
|
3361
|
+
<div class="studio-camera-range-head"><span>duration (ms)</span><span class="studio-camera-range-value">${entry.duration}</span></div>
|
|
3362
|
+
<div class="studio-camera-chips">${preset(0, "\uCEF7")}${preset(300, "300")}${preset(600, "600")}${preset(1e3, "1000")}${preset(1600, "1600")}</div>
|
|
3363
|
+
<div class="studio-camera-range-row">
|
|
3364
|
+
<input type="range" data-prop-key="camera.focus.${index}.duration" min="0" max="3000" step="50" value="${entry.duration}" />
|
|
3365
|
+
<input type="number" data-prop-key="camera.focus.${index}.duration" step="50" value="${entry.duration}" />
|
|
3366
|
+
</div>
|
|
3367
|
+
</div>
|
|
3368
|
+
<div class="studio-camera-field-label">\uB300\uC0C1 \uC694\uC18C (${entry.elementIds.length})</div>
|
|
3369
|
+
${focusTargetPicker(def, index, entry.elementIds)}
|
|
3370
|
+
${rangeField("padding", `camera.focus.${index}.padding`, entry.padding, 0, 200, 2, "\uB300\uC0C1 \uC8FC\uBCC0 \uC5EC\uBC31. \uD074\uC218\uB85D \uB113\uAC8C \uC7A1\uC2B5\uB2C8\uB2E4.")}
|
|
3371
|
+
${rangeField("maxZoom", `camera.focus.${index}.maxZoom`, entry.maxZoom, 1, 8, 0.1, "\uD655\uB300 \uC0C1\uD55C. \uC791\uC740 \uC694\uC18C \uD558\uB098\uAC00 \uD654\uBA74\uC744 \uCC44\uC6B0\uB294 \uAC83\uC744 \uB9C9\uC2B5\uB2C8\uB2E4.")}
|
|
3372
|
+
<button type="button" class="studio-btn studio-btn-danger" data-delete-camera-focus="${index}">\u{1F5D1} focus \uC0AD\uC81C</button>
|
|
3373
|
+
</details>`;
|
|
3374
|
+
}).join("");
|
|
3375
|
+
const trackRows = ["zoom", "x", "y"].map((property) => {
|
|
3376
|
+
const track = camera.tracks.find((t) => t.property === property);
|
|
3377
|
+
const current = property === "zoom" ? view?.zoom ?? 1 : property === "x" ? view?.centerX ?? def.canvas.width / 2 : view?.centerY ?? def.canvas.height / 2;
|
|
3378
|
+
const keyframes = (track?.keyframes ?? []).map(
|
|
3379
|
+
(kf) => `<li><button type="button" class="studio-camera-kf" data-camera-seek="${kf.time}">${kf.time}ms</button><span>${kf.value}</span><button type="button" class="studio-camera-kf-del" data-camera-kf-del="${property}:${kf.time}" title="keyframe \uC0AD\uC81C">\u2715</button></li>`
|
|
3380
|
+
).join("");
|
|
3381
|
+
const bounds = property === "zoom" ? { min: 0.1, max: 8, step: 0.05 } : property === "x" ? { min: 0, max: def.canvas.width, step: 1 } : { min: 0, max: def.canvas.height, step: 1 };
|
|
3382
|
+
return `
|
|
3383
|
+
<div class="studio-camera-track">
|
|
3384
|
+
<div class="studio-camera-track-head">
|
|
3385
|
+
<span>${property}</span>
|
|
3386
|
+
<span class="studio-camera-track-now">\uD604\uC7AC ${current.toFixed(property === "zoom" ? 2 : 0)}</span>
|
|
3387
|
+
${track ? `<button type="button" class="studio-btn studio-btn-danger" data-del-camera-track="${property}" title="track \uC0AD\uC81C">\u{1F5D1}</button>` : ""}
|
|
3388
|
+
</div>
|
|
3389
|
+
${control.kind === "focus" ? `<div class="studio-camera-range-row">
|
|
3390
|
+
<input type="number" data-prop-key="camera.kf.${property}" step="${bounds.step}" value="${current.toFixed(property === "zoom" ? 2 : 0)}" />
|
|
3391
|
+
</div>
|
|
3392
|
+
<p class="studio-camera-hint">focus #${control.index + 1}\uC774(\uAC00) \uC774 \uC2DC\uAC01\uC744 \uACB0\uC815\uD558\uBBC0\uB85C \uC9C0\uAE08\uC740 \uD654\uBA74\uC774 \uBC14\uB00C\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uAC12\uC740 t = ${time}ms \uC5D0 \uAE30\uB85D\uB429\uB2C8\uB2E4.</p>` : `<div class="studio-camera-range-row">
|
|
3393
|
+
<input type="range" data-prop-key="camera.kf.${property}" min="${bounds.min}" max="${bounds.max}" step="${bounds.step}" value="${current.toFixed(property === "zoom" ? 2 : 0)}" />
|
|
3394
|
+
<input type="number" data-prop-key="camera.kf.${property}" step="${bounds.step}" value="${current.toFixed(property === "zoom" ? 2 : 0)}" />
|
|
3395
|
+
</div>
|
|
3396
|
+
<p class="studio-camera-hint">\uC6C0\uC9C1\uC774\uBA74 t = ${time}ms \uC5D0 keyframe\uC744 \uC501\uB2C8\uB2E4.</p>`}
|
|
3397
|
+
${keyframes ? `<ul class="studio-camera-kf-list">${keyframes}</ul>` : '<p class="studio-props-empty">keyframe \uC5C6\uC74C</p>'}
|
|
3398
|
+
</div>`;
|
|
3399
|
+
}).join("");
|
|
3400
|
+
panelEl.innerHTML = `
|
|
3401
|
+
<span class="studio-step-hint">\u{1F4CD} t = ${time} ms / ${def.duration} ms</span>
|
|
3402
|
+
<div class="studio-props-header"><span class="studio-props-header-title">\uCE74\uBA54\uB77C</span><span class="studio-props-header-type">camera</span></div>
|
|
3403
|
+
${readout}
|
|
3404
|
+
<label class="studio-field"><span>strokeScaling</span><select data-prop-key="camera.strokeScaling">${scalingOpts}</select></label>
|
|
3405
|
+
<div class="studio-props-header" style="margin-top:0.6rem"><span class="studio-props-header-title">focus (${camera.focus.length})</span></div>
|
|
3406
|
+
<p class="studio-props-empty" style="margin:0 0 0.4rem">\uC694\uC18C\uB97C \uC9C0\uC815\uD558\uBA74 \uADF8 \uC694\uC18C\uAC00 \uD654\uBA74\uC5D0 \uB2F4\uAE30\uB3C4\uB85D \uCE74\uBA54\uB77C\uB97C \uACC4\uC0B0\uD569\uB2C8\uB2E4. \uB300\uC0C1\uC774 \uC6C0\uC9C1\uC774\uBA74 \uB530\uB77C\uAC11\uB2C8\uB2E4.</p>
|
|
3407
|
+
${focusRows}
|
|
3408
|
+
<button type="button" class="studio-btn" data-add-camera-focus style="margin-top:0.4rem">\uFF0B \uD604\uC7AC \uC2DC\uAC01\uC5D0 focus</button>
|
|
3409
|
+
<div class="studio-props-header" style="margin-top:0.6rem"><span class="studio-props-header-title">tracks</span></div>
|
|
3410
|
+
${trackRows}
|
|
3411
|
+
${hasCamera() ? '<button type="button" class="studio-btn studio-btn-danger" data-clear-camera style="margin-top:0.8rem">\u{1F5D1} \uCE74\uBA54\uB77C \uC81C\uAC70</button>' : ""}
|
|
3412
|
+
`;
|
|
3413
|
+
}
|
|
2965
3414
|
function renderElementForm(def, el) {
|
|
2966
3415
|
if (!panelEl) return;
|
|
2967
3416
|
const timeHint = `<span class="studio-step-hint">\u{1F4CD} t = ${getCurrentTime()} ms</span>`;
|
|
@@ -2988,6 +3437,7 @@ function renderElementForm(def, el) {
|
|
|
2988
3437
|
const baseFields = renderBaseFields(def, el);
|
|
2989
3438
|
const appearances = renderAppearances(def, el);
|
|
2990
3439
|
const tracks = renderTracks(el);
|
|
3440
|
+
const bindings = renderBindings(el);
|
|
2991
3441
|
const baseHeader = `<span class="studio-props-header-title">${escapeHtml2(el.id)}</span><span class="studio-props-header-type">${escapeHtml2(el.type)}</span>`;
|
|
2992
3442
|
const apHeader = `<span class="studio-props-header-title">\uCD9C\uD604 (Appearances)</span><button type="button" class="studio-btn studio-btn-small" data-add-appearance>\uFF0B</button>`;
|
|
2993
3443
|
const tracksHeader = `<span class="studio-props-header-title">\uD0A4\uD504\uB808\uC784 \uD2B8\uB799 (${el.tracks.length})</span>`;
|
|
@@ -2996,12 +3446,45 @@ function renderElementForm(def, el) {
|
|
|
2996
3446
|
${section("el-base", baseHeader, baseFields)}
|
|
2997
3447
|
${section("el-appearances", apHeader, appearances)}
|
|
2998
3448
|
${section("el-tracks", tracksHeader, tracks)}
|
|
3449
|
+
${section("el-bindings", `<span class="studio-props-header-title">\uB370\uC774\uD130 \uC5F0\uACB0 (${el.bindings.length})</span>`, bindings)}
|
|
2999
3450
|
<div class="studio-props-empty" style="font-size:0.72rem;margin-top:0.5rem">
|
|
3000
3451
|
base \uC18D\uC131\uC744 \uBCC0\uACBD\uD558\uBA74 \u2192 t=${getCurrentTime()} ms \uC5D0 keyframe \uCD94\uAC00<br/>
|
|
3001
3452
|
\uD2B8\uB799\uC774 \uC5C6\uB294 \uC18D\uC131\uC740 base \uAC12\uC774 \uD56D\uC0C1 \uC0AC\uC6A9\uB428
|
|
3002
3453
|
</div>
|
|
3003
3454
|
`;
|
|
3004
3455
|
}
|
|
3456
|
+
function renderBindings(el) {
|
|
3457
|
+
const properties = bindablePropertiesFor(el);
|
|
3458
|
+
const rows = el.bindings.map(
|
|
3459
|
+
(binding, index) => `<div class="studio-appearance-row">
|
|
3460
|
+
<div class="studio-appearance-row-head"><span class="studio-appearance-row-title">#${index + 1}</span><button type="button" class="studio-btn studio-btn-small studio-btn-danger" data-delete-binding="${index}">\u2715</button></div>
|
|
3461
|
+
${selectField(
|
|
3462
|
+
"\uC18D\uC131",
|
|
3463
|
+
`binding.${index}.property`,
|
|
3464
|
+
binding.property,
|
|
3465
|
+
properties.map((value) => ({ value }))
|
|
3466
|
+
)}
|
|
3467
|
+
${textField("JSON Pointer", `binding.${index}.pointer`, binding.pointer)}
|
|
3468
|
+
${selectField(
|
|
3469
|
+
"\uD45C\uD604 \uBC29\uC2DD",
|
|
3470
|
+
`binding.${index}.formatter`,
|
|
3471
|
+
binding.formatter,
|
|
3472
|
+
[
|
|
3473
|
+
"identity",
|
|
3474
|
+
"string",
|
|
3475
|
+
"number",
|
|
3476
|
+
"fixed",
|
|
3477
|
+
"percent",
|
|
3478
|
+
"uppercase",
|
|
3479
|
+
"lowercase",
|
|
3480
|
+
"color"
|
|
3481
|
+
].map((value) => ({ value }))
|
|
3482
|
+
)}
|
|
3483
|
+
${textField("\uB300\uCCB4 \uAC12", `binding.${index}.fallback`, binding.fallback === void 0 ? "" : String(binding.fallback))}
|
|
3484
|
+
</div>`
|
|
3485
|
+
).join("");
|
|
3486
|
+
return `${rows || '<p class="studio-props-empty">\uC5F0\uACB0\uB41C \uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</p>'}<button type="button" class="studio-btn" data-add-binding ${properties.length === 0 ? "disabled" : ""}>\uFF0B \uB370\uC774\uD130 \uC5F0\uACB0</button>`;
|
|
3487
|
+
}
|
|
3005
3488
|
function renderBaseFields(def, el) {
|
|
3006
3489
|
const numberFields = [];
|
|
3007
3490
|
const colorFields = [];
|
|
@@ -3133,6 +3616,11 @@ function renderBaseFields(def, el) {
|
|
|
3133
3616
|
`el.translation.${locale}`,
|
|
3134
3617
|
localized.translations?.[locale] ?? ""
|
|
3135
3618
|
)
|
|
3619
|
+
),
|
|
3620
|
+
...annotationReferenceFields(
|
|
3621
|
+
"el",
|
|
3622
|
+
[el.content, ...Object.values(localized.translations ?? {})],
|
|
3623
|
+
localized.references
|
|
3136
3624
|
)
|
|
3137
3625
|
];
|
|
3138
3626
|
})();
|
|
@@ -3204,11 +3692,16 @@ function renderTracks(el) {
|
|
|
3204
3692
|
}
|
|
3205
3693
|
function onInput(e) {
|
|
3206
3694
|
const target = e.target;
|
|
3695
|
+
const focusTarget = target.dataset.cameraTarget;
|
|
3696
|
+
if (focusTarget !== void 0 && target instanceof HTMLInputElement) {
|
|
3697
|
+
toggleFocusTarget(Number(focusTarget), target.value, target.checked);
|
|
3698
|
+
return;
|
|
3699
|
+
}
|
|
3207
3700
|
const key = target.dataset.propKey;
|
|
3208
3701
|
if (!key) return;
|
|
3209
3702
|
if (target.type === "text" && e.isComposing) return;
|
|
3210
3703
|
if (target.type === "text" && isComposingFallback) return;
|
|
3211
|
-
if (target.type === "color") {
|
|
3704
|
+
if (target instanceof HTMLInputElement && target.type === "color") {
|
|
3212
3705
|
const textInput = target.parentElement?.querySelector(
|
|
3213
3706
|
`input[type="text"][data-prop-key="${CSS.escape(key)}"]`
|
|
3214
3707
|
);
|
|
@@ -3216,10 +3709,22 @@ function onInput(e) {
|
|
|
3216
3709
|
return;
|
|
3217
3710
|
}
|
|
3218
3711
|
let value = target.value;
|
|
3219
|
-
if (target.type === "number")
|
|
3220
|
-
|
|
3712
|
+
if (target instanceof HTMLInputElement && target.type === "number")
|
|
3713
|
+
value = Number(target.value);
|
|
3714
|
+
else if (target instanceof HTMLInputElement && target.type === "checkbox")
|
|
3715
|
+
value = target.checked;
|
|
3221
3716
|
apply(key, value);
|
|
3222
3717
|
}
|
|
3718
|
+
function toggleFocusTarget(index, id, on) {
|
|
3719
|
+
const entry = getCamera().focus[index];
|
|
3720
|
+
if (!entry) return;
|
|
3721
|
+
const next = on ? [...entry.elementIds, id] : entry.elementIds.filter((value) => value !== id);
|
|
3722
|
+
if (next.length === 0) {
|
|
3723
|
+
render3();
|
|
3724
|
+
return;
|
|
3725
|
+
}
|
|
3726
|
+
updateCameraFocus(index, { elementIds: next });
|
|
3727
|
+
}
|
|
3223
3728
|
function onChange(e) {
|
|
3224
3729
|
const target = e.target;
|
|
3225
3730
|
if (target instanceof HTMLInputElement && target.type === "color") {
|
|
@@ -3239,6 +3744,22 @@ function apply(key, value) {
|
|
|
3239
3744
|
else if (key === "meta.locales") {
|
|
3240
3745
|
const locales = parseLocales(String(value));
|
|
3241
3746
|
if (locales.length > 0) updateLocales(locales);
|
|
3747
|
+
} else if (key === "meta.data") {
|
|
3748
|
+
try {
|
|
3749
|
+
const parsed = JSON.parse(String(value));
|
|
3750
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed))
|
|
3751
|
+
updateData(parsed);
|
|
3752
|
+
} catch {
|
|
3753
|
+
}
|
|
3754
|
+
} else if (key === "meta.responsive") {
|
|
3755
|
+
try {
|
|
3756
|
+
const parsed = JSON.parse(String(value));
|
|
3757
|
+
if (Array.isArray(parsed))
|
|
3758
|
+
updateResponsive(
|
|
3759
|
+
parsed
|
|
3760
|
+
);
|
|
3761
|
+
} catch {
|
|
3762
|
+
}
|
|
3242
3763
|
} else if (key === "meta.duration") updateDuration(Number(value));
|
|
3243
3764
|
else if (key === "canvas.width") updateCanvas({ width: Number(value) });
|
|
3244
3765
|
else if (key === "canvas.height") updateCanvas({ height: Number(value) });
|
|
@@ -3255,7 +3776,83 @@ function apply(key, value) {
|
|
|
3255
3776
|
updateSettings({
|
|
3256
3777
|
chapterListPosition: String(value)
|
|
3257
3778
|
});
|
|
3258
|
-
else if (key.startsWith("
|
|
3779
|
+
else if (key.startsWith("checkpoint.")) {
|
|
3780
|
+
const [, indexText, property] = key.split(".");
|
|
3781
|
+
const checkpoint = def.checkpoints[Number(indexText)];
|
|
3782
|
+
if (!checkpoint || !property) return;
|
|
3783
|
+
if (property === "interaction") {
|
|
3784
|
+
const base = {
|
|
3785
|
+
id: checkpoint.id,
|
|
3786
|
+
time: checkpoint.time,
|
|
3787
|
+
prompt: checkpoint.prompt,
|
|
3788
|
+
required: checkpoint.required
|
|
3789
|
+
};
|
|
3790
|
+
const interaction = String(value);
|
|
3791
|
+
const replacement = interaction === "choice" ? {
|
|
3792
|
+
...base,
|
|
3793
|
+
interaction,
|
|
3794
|
+
options: [{ value: "option", label: "\uC120\uD0DD\uC9C0" }]
|
|
3795
|
+
} : interaction === "select-element" ? {
|
|
3796
|
+
...base,
|
|
3797
|
+
interaction,
|
|
3798
|
+
elementIds: [def.elements[0]?.id ?? "element"]
|
|
3799
|
+
} : interaction === "number-input" ? { ...base, interaction } : { ...base, interaction: "continue" };
|
|
3800
|
+
updateCheckpoint(checkpoint.id, replacement);
|
|
3801
|
+
} else if (property === "time")
|
|
3802
|
+
updateCheckpoint(checkpoint.id, { time: Number(value) });
|
|
3803
|
+
else if (property === "prompt")
|
|
3804
|
+
updateCheckpoint(checkpoint.id, { prompt: String(value) });
|
|
3805
|
+
else if (property === "required")
|
|
3806
|
+
updateCheckpoint(checkpoint.id, { required: Boolean(value) });
|
|
3807
|
+
else if (property === "options" && checkpoint.interaction === "choice") {
|
|
3808
|
+
const options = String(value).split(",").map((entry) => entry.trim()).filter(Boolean).map((entry) => {
|
|
3809
|
+
const [optionValue, ...label] = entry.split(":");
|
|
3810
|
+
return {
|
|
3811
|
+
value: optionValue || "option",
|
|
3812
|
+
label: label.join(":") || optionValue || "\uC120\uD0DD\uC9C0"
|
|
3813
|
+
};
|
|
3814
|
+
});
|
|
3815
|
+
if (options.length > 0) updateCheckpoint(checkpoint.id, { options });
|
|
3816
|
+
} else if (property === "elementIds" && checkpoint.interaction === "select-element") {
|
|
3817
|
+
const elementIds = String(value).split(",").map((id) => id.trim()).filter(Boolean);
|
|
3818
|
+
if (elementIds.length > 0)
|
|
3819
|
+
updateCheckpoint(checkpoint.id, { elementIds });
|
|
3820
|
+
} else if (["min", "max", "step"].includes(property) && checkpoint.interaction === "number-input") {
|
|
3821
|
+
updateCheckpoint(checkpoint.id, { [property]: Number(value) });
|
|
3822
|
+
} else if (property === "answer" && checkpoint.interaction !== "continue") {
|
|
3823
|
+
const answer = checkpoint.interaction === "number-input" ? Number(value) : String(value);
|
|
3824
|
+
updateCheckpoint(checkpoint.id, {
|
|
3825
|
+
predicate: { type: "equals", value: answer }
|
|
3826
|
+
});
|
|
3827
|
+
}
|
|
3828
|
+
} else if (key === "camera.strokeScaling")
|
|
3829
|
+
setCameraStrokeScaling(String(value));
|
|
3830
|
+
else if (key.startsWith("camera.kf.")) {
|
|
3831
|
+
const property = key.slice("camera.kf.".length);
|
|
3832
|
+
const raw = Number(value);
|
|
3833
|
+
if (!Number.isFinite(raw)) return;
|
|
3834
|
+
setCameraKeyframe(
|
|
3835
|
+
property,
|
|
3836
|
+
getCurrentTime(),
|
|
3837
|
+
property === "zoom" ? Math.max(0.05, raw) : Math.round(raw)
|
|
3838
|
+
);
|
|
3839
|
+
} else if (key.startsWith("camera.focus.")) {
|
|
3840
|
+
const [, , indexText, field] = key.split(".");
|
|
3841
|
+
const index = Number(indexText);
|
|
3842
|
+
if (!Number.isInteger(index)) return;
|
|
3843
|
+
if (field === "elementIds") {
|
|
3844
|
+
const ids = String(value).split(",").map((id) => id.trim()).filter(Boolean);
|
|
3845
|
+
if (ids.length > 0) updateCameraFocus(index, { elementIds: ids });
|
|
3846
|
+
return;
|
|
3847
|
+
}
|
|
3848
|
+
if (field === "time") updateCameraFocus(index, { time: Number(value) });
|
|
3849
|
+
else if (field === "duration")
|
|
3850
|
+
updateCameraFocus(index, { duration: Math.max(0, Number(value)) });
|
|
3851
|
+
else if (field === "padding")
|
|
3852
|
+
updateCameraFocus(index, { padding: Math.max(0, Number(value)) });
|
|
3853
|
+
else if (field === "maxZoom")
|
|
3854
|
+
updateCameraFocus(index, { maxZoom: Math.max(0.01, Number(value)) });
|
|
3855
|
+
} else if (key.startsWith("el.") && sel.kind === "element") {
|
|
3259
3856
|
const prop = key.slice(3);
|
|
3260
3857
|
const time = getCurrentTime();
|
|
3261
3858
|
const el = def.elements.find((e) => e.id === sel.elementId);
|
|
@@ -3276,6 +3873,16 @@ function apply(key, value) {
|
|
|
3276
3873
|
updateElementBase(sel.elementId, { translations });
|
|
3277
3874
|
return;
|
|
3278
3875
|
}
|
|
3876
|
+
if (prop.startsWith("reference.") && el.type === "text") {
|
|
3877
|
+
const token = prop.slice("reference.".length);
|
|
3878
|
+
const annotated = el;
|
|
3879
|
+
const references = { ...annotated.references ?? {} };
|
|
3880
|
+
const targets = parseReferenceTargets(String(value));
|
|
3881
|
+
if (targets) references[token] = targets;
|
|
3882
|
+
else delete references[token];
|
|
3883
|
+
updateElementBase(sel.elementId, { references });
|
|
3884
|
+
return;
|
|
3885
|
+
}
|
|
3279
3886
|
if (prop === "polygonSides" && el.type === "polygon") {
|
|
3280
3887
|
const points = el.points.trim().split(/\s+/).map((point) => point.split(",").map(Number)).filter(([x, y]) => Number.isFinite(x) && Number.isFinite(y));
|
|
3281
3888
|
if (points.length < 3) return;
|
|
@@ -3300,6 +3907,18 @@ function apply(key, value) {
|
|
|
3300
3907
|
} else {
|
|
3301
3908
|
updateElementBase(sel.elementId, { [prop]: value });
|
|
3302
3909
|
}
|
|
3910
|
+
} else if (key.startsWith("binding.") && sel.kind === "element") {
|
|
3911
|
+
const [, indexText, property] = key.split(".");
|
|
3912
|
+
const el = def.elements.find((item) => item.id === sel.elementId);
|
|
3913
|
+
const binding = el?.bindings[Number(indexText)];
|
|
3914
|
+
if (!el || !binding || !property) return;
|
|
3915
|
+
const bindings = el.bindings.map(
|
|
3916
|
+
(item, index) => index === Number(indexText) ? {
|
|
3917
|
+
...item,
|
|
3918
|
+
[property]: property === "fallback" ? parseBindingFallback(String(value)) : String(value)
|
|
3919
|
+
} : item
|
|
3920
|
+
);
|
|
3921
|
+
updateElementBase(el.id, { bindings });
|
|
3303
3922
|
} else if (key.startsWith("ap.") && sel.kind === "element") {
|
|
3304
3923
|
const [, idxStr, prop] = key.split(".");
|
|
3305
3924
|
const idx = Number(idxStr);
|
|
@@ -3317,6 +3936,16 @@ function apply(key, value) {
|
|
|
3317
3936
|
updateChapter(sel.chapterId, { label: String(value) });
|
|
3318
3937
|
} else if (key === "chapter.subtitle" && sel.kind === "chapter") {
|
|
3319
3938
|
updateChapter(sel.chapterId, { subtitle: String(value) });
|
|
3939
|
+
} else if (key.startsWith("chapter.reference.") && sel.kind === "chapter") {
|
|
3940
|
+
const chapter = def.chapters.find(({ id }) => id === sel.chapterId);
|
|
3941
|
+
if (!chapter) return;
|
|
3942
|
+
const token = key.slice("chapter.reference.".length);
|
|
3943
|
+
const annotated = chapter;
|
|
3944
|
+
const references = { ...annotated.references ?? {} };
|
|
3945
|
+
const targets = parseReferenceTargets(String(value));
|
|
3946
|
+
if (targets) references[token] = targets;
|
|
3947
|
+
else delete references[token];
|
|
3948
|
+
updateChapter(sel.chapterId, { references });
|
|
3320
3949
|
} else if (key.startsWith("effect.") && sel.kind === "effect") {
|
|
3321
3950
|
const prop = key.slice(7);
|
|
3322
3951
|
const patch = {};
|
|
@@ -3328,11 +3957,44 @@ function apply(key, value) {
|
|
|
3328
3957
|
updateEffect(sel.effectId, patch);
|
|
3329
3958
|
}
|
|
3330
3959
|
}
|
|
3960
|
+
function parseBindingFallback(value) {
|
|
3961
|
+
if (value === "") return void 0;
|
|
3962
|
+
if (value === "true") return true;
|
|
3963
|
+
if (value === "false") return false;
|
|
3964
|
+
const number = Number(value);
|
|
3965
|
+
return Number.isFinite(number) && value.trim() !== "" ? number : value;
|
|
3966
|
+
}
|
|
3331
3967
|
function onClick2(e) {
|
|
3332
3968
|
const target = e.target;
|
|
3333
3969
|
const def = getDef();
|
|
3334
3970
|
if (!def) return;
|
|
3335
3971
|
const sel = getSelection();
|
|
3972
|
+
if (target.closest("[data-add-binding]") && sel.kind === "element") {
|
|
3973
|
+
const el = def.elements.find((item) => item.id === sel.elementId);
|
|
3974
|
+
if (!el) return;
|
|
3975
|
+
const property = bindablePropertiesFor(el).find(
|
|
3976
|
+
(candidate) => !el.bindings.some((binding) => binding.property === candidate)
|
|
3977
|
+
);
|
|
3978
|
+
if (property)
|
|
3979
|
+
updateElementBase(el.id, {
|
|
3980
|
+
bindings: [
|
|
3981
|
+
...el.bindings,
|
|
3982
|
+
{ property, pointer: "", formatter: "identity" }
|
|
3983
|
+
]
|
|
3984
|
+
});
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
3987
|
+
const deleteBinding = target.closest("[data-delete-binding]");
|
|
3988
|
+
if (deleteBinding && sel.kind === "element") {
|
|
3989
|
+
const el = def.elements.find((item) => item.id === sel.elementId);
|
|
3990
|
+
if (el)
|
|
3991
|
+
updateElementBase(el.id, {
|
|
3992
|
+
bindings: el.bindings.filter(
|
|
3993
|
+
(_, index) => index !== Number(deleteBinding.dataset.deleteBinding)
|
|
3994
|
+
)
|
|
3995
|
+
});
|
|
3996
|
+
return;
|
|
3997
|
+
}
|
|
3336
3998
|
const alignBtn = target.closest("[data-align]");
|
|
3337
3999
|
if (alignBtn) {
|
|
3338
4000
|
alignSelected(alignBtn.dataset.align);
|
|
@@ -3343,6 +4005,37 @@ function onClick2(e) {
|
|
|
3343
4005
|
distributeSelected(distBtn.dataset.distribute);
|
|
3344
4006
|
return;
|
|
3345
4007
|
}
|
|
4008
|
+
const createLayoutButton = target.closest(
|
|
4009
|
+
"[data-create-layout]"
|
|
4010
|
+
);
|
|
4011
|
+
if (createLayoutButton && sel.kind === "elements") {
|
|
4012
|
+
createLayout(
|
|
4013
|
+
sel.elementIds,
|
|
4014
|
+
createLayoutButton.dataset.createLayout
|
|
4015
|
+
);
|
|
4016
|
+
return;
|
|
4017
|
+
}
|
|
4018
|
+
if (target.closest("[data-detach-layout]") && sel.kind === "elements") {
|
|
4019
|
+
detachFromLayout(sel.elementIds);
|
|
4020
|
+
return;
|
|
4021
|
+
}
|
|
4022
|
+
if (target.closest("[data-add-checkpoint]")) {
|
|
4023
|
+
addCheckpoint({
|
|
4024
|
+
id: uniqueCheckpointId(),
|
|
4025
|
+
time: getCurrentTime(),
|
|
4026
|
+
prompt: "\uACC4\uC18D \uC9C4\uD589\uD560\uAE4C\uC694?",
|
|
4027
|
+
required: true,
|
|
4028
|
+
interaction: "continue"
|
|
4029
|
+
});
|
|
4030
|
+
return;
|
|
4031
|
+
}
|
|
4032
|
+
const deleteCheckpointButton = target.closest(
|
|
4033
|
+
"[data-delete-checkpoint]"
|
|
4034
|
+
);
|
|
4035
|
+
if (deleteCheckpointButton?.dataset.deleteCheckpoint) {
|
|
4036
|
+
deleteCheckpoint(deleteCheckpointButton.dataset.deleteCheckpoint);
|
|
4037
|
+
return;
|
|
4038
|
+
}
|
|
3346
4039
|
if (target.closest("[data-ungroup]") && sel.kind === "element") {
|
|
3347
4040
|
ungroupElement(sel.elementId);
|
|
3348
4041
|
return;
|
|
@@ -3356,13 +4049,82 @@ function onClick2(e) {
|
|
|
3356
4049
|
});
|
|
3357
4050
|
return;
|
|
3358
4051
|
}
|
|
4052
|
+
if (target.closest("[data-open-camera]")) {
|
|
4053
|
+
setSelection({ kind: "camera" });
|
|
4054
|
+
return;
|
|
4055
|
+
}
|
|
4056
|
+
if (target.closest("[data-add-camera-focus]")) {
|
|
4057
|
+
const seed = sel.kind === "element" ? [sel.elementId] : sel.kind === "elements" ? [...sel.elementIds] : def.elements[0] ? [def.elements[0].id] : [];
|
|
4058
|
+
if (seed.length === 0) return;
|
|
4059
|
+
addCameraFocus({
|
|
4060
|
+
time: getCurrentTime(),
|
|
4061
|
+
duration: 600,
|
|
4062
|
+
elementIds: seed,
|
|
4063
|
+
padding: 24,
|
|
4064
|
+
maxZoom: 4
|
|
4065
|
+
});
|
|
4066
|
+
return;
|
|
4067
|
+
}
|
|
4068
|
+
const durationChip = target.closest("[data-camera-duration]");
|
|
4069
|
+
if (durationChip) {
|
|
4070
|
+
const [index, duration] = (durationChip.dataset.cameraDuration ?? "").split(
|
|
4071
|
+
":"
|
|
4072
|
+
);
|
|
4073
|
+
updateCameraFocus(Number(index), { duration: Number(duration) });
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4076
|
+
const focusNow = target.closest("[data-camera-focus-now]");
|
|
4077
|
+
if (focusNow) {
|
|
4078
|
+
updateCameraFocus(Number(focusNow.dataset.cameraFocusNow), {
|
|
4079
|
+
time: getCurrentTime()
|
|
4080
|
+
});
|
|
4081
|
+
return;
|
|
4082
|
+
}
|
|
4083
|
+
const deleteFocus = target.closest("[data-delete-camera-focus]");
|
|
4084
|
+
if (deleteFocus) {
|
|
4085
|
+
deleteCameraFocus(Number(deleteFocus.dataset.deleteCameraFocus));
|
|
4086
|
+
return;
|
|
4087
|
+
}
|
|
4088
|
+
const addCameraKf = target.closest("[data-add-camera-kf]");
|
|
4089
|
+
if (addCameraKf) {
|
|
4090
|
+
const property = addCameraKf.dataset.addCameraKf;
|
|
4091
|
+
const view = computeCamera(def, getCurrentTime());
|
|
4092
|
+
const value = property === "zoom" ? view?.zoom ?? 1 : property === "x" ? view?.centerX ?? def.canvas.width / 2 : view?.centerY ?? def.canvas.height / 2;
|
|
4093
|
+
setCameraKeyframe(
|
|
4094
|
+
property,
|
|
4095
|
+
getCurrentTime(),
|
|
4096
|
+
Number(value.toFixed(property === "zoom" ? 3 : 1))
|
|
4097
|
+
);
|
|
4098
|
+
return;
|
|
4099
|
+
}
|
|
4100
|
+
const delCameraKf = target.closest("[data-camera-kf-del]");
|
|
4101
|
+
if (delCameraKf) {
|
|
4102
|
+
const [property, at] = (delCameraKf.dataset.cameraKfDel ?? "").split(":");
|
|
4103
|
+
removeCameraKeyframe(property, Number(at));
|
|
4104
|
+
return;
|
|
4105
|
+
}
|
|
4106
|
+
const delCameraTrack = target.closest("[data-del-camera-track]");
|
|
4107
|
+
if (delCameraTrack) {
|
|
4108
|
+
removeCameraTrack(delCameraTrack.dataset.delCameraTrack);
|
|
4109
|
+
return;
|
|
4110
|
+
}
|
|
4111
|
+
const cameraSeek = target.closest("[data-camera-seek]");
|
|
4112
|
+
if (cameraSeek) {
|
|
4113
|
+
setCurrentTime(Number(cameraSeek.dataset.cameraSeek));
|
|
4114
|
+
return;
|
|
4115
|
+
}
|
|
4116
|
+
if (target.closest("[data-clear-camera]")) {
|
|
4117
|
+
clearCamera();
|
|
4118
|
+
return;
|
|
4119
|
+
}
|
|
3359
4120
|
if (target.closest("[data-add-chapter]")) {
|
|
3360
4121
|
const id = uniqueChapterId();
|
|
3361
4122
|
addChapter({
|
|
3362
4123
|
id,
|
|
3363
4124
|
time: getCurrentTime(),
|
|
3364
4125
|
label: `Chapter ${id.split("-")[1]}`,
|
|
3365
|
-
subtitle: ""
|
|
4126
|
+
subtitle: "",
|
|
4127
|
+
references: {}
|
|
3366
4128
|
});
|
|
3367
4129
|
return;
|
|
3368
4130
|
}
|
|
@@ -3497,7 +4259,8 @@ function initTimeline(tracksRoot, addChapterBtn, elementTracks) {
|
|
|
3497
4259
|
id,
|
|
3498
4260
|
time: newTime,
|
|
3499
4261
|
label: `Chapter ${id.split("-")[1]}`,
|
|
3500
|
-
subtitle: ""
|
|
4262
|
+
subtitle: "",
|
|
4263
|
+
references: {}
|
|
3501
4264
|
});
|
|
3502
4265
|
setSelection({ kind: "chapter", chapterId: id });
|
|
3503
4266
|
});
|
|
@@ -3602,6 +4365,7 @@ function render4() {
|
|
|
3602
4365
|
<div class="studio-tl-chapter-label">${escapeHtml3(c.label || c.id)}</div>
|
|
3603
4366
|
</div>`;
|
|
3604
4367
|
}).join("");
|
|
4368
|
+
const cameraRows = renderCameraRows(totalPx, sel);
|
|
3605
4369
|
const playheadCol = `<div class="studio-tl-playhead-col" style="left:${GUTTER_PX}px;width:${totalPx}px"><div class="studio-tl-playhead" style="left:${timeToPx(currentTime)}px" title="t=${currentTime}ms"></div></div>`;
|
|
3606
4370
|
tracksEl.innerHTML = `
|
|
3607
4371
|
<div class="studio-tl-chart">
|
|
@@ -3619,12 +4383,48 @@ function render4() {
|
|
|
3619
4383
|
</div>
|
|
3620
4384
|
</div>
|
|
3621
4385
|
</div>
|
|
4386
|
+
${cameraRows}
|
|
3622
4387
|
${playheadCol}
|
|
3623
4388
|
</div>
|
|
3624
4389
|
<div class="studio-tl-total">\uC804\uCCB4 ${def.duration} ms \xB7 ${sortedChapters.length} chapters \xB7 ${def.elements.length} elements \xB7 \uD604\uC7AC ${currentTime} ms</div>
|
|
3625
4390
|
`;
|
|
3626
4391
|
renderElementTracks(def.elements, currentTime, totalPx, sel);
|
|
3627
4392
|
}
|
|
4393
|
+
function renderCameraRows(totalPx, sel) {
|
|
4394
|
+
const camera = getCamera();
|
|
4395
|
+
const isCameraSel = sel.kind === "camera";
|
|
4396
|
+
const focusBars = camera.focus.map((entry, index) => {
|
|
4397
|
+
const left = timeToPx(entry.time);
|
|
4398
|
+
const width = Math.max(6, timeToPx(entry.duration));
|
|
4399
|
+
const selected = isCameraSel && sel.focusIndex === index;
|
|
4400
|
+
const targets = entry.elementIds.join(", ");
|
|
4401
|
+
return `<div class="studio-tl-camera-focus ${selected ? "is-selected" : ""}" style="left:${left}px;width:${width}px" data-camera-focus="${index}" title="focus \u2192 ${escapeHtml3(targets)} @ ${entry.time}ms (${entry.duration}ms, \uB4DC\uB798\uADF8\uB85C \uC774\uB3D9)">
|
|
4402
|
+
<span class="studio-tl-camera-focus-label">${escapeHtml3(targets)}</span>
|
|
4403
|
+
</div>`;
|
|
4404
|
+
}).join("");
|
|
4405
|
+
const focusRow = `
|
|
4406
|
+
<div class="studio-tl-row studio-tl-camera-row ${isCameraSel && sel.focusIndex === void 0 ? "is-selected" : ""}">
|
|
4407
|
+
<div class="studio-tl-gutter studio-tl-camera-gutter" data-camera-select title="\uCE74\uBA54\uB77C \uC18D\uC131 \uC5F4\uAE30">\u{1F3A5} Camera<button type="button" class="studio-tl-camera-add" data-add-camera-focus title="\uD604\uC7AC \uC2DC\uAC01\uC5D0 focus \uCD94\uAC00">\uFF0B</button></div>
|
|
4408
|
+
<div class="studio-tl-body" style="width:${totalPx}px">
|
|
4409
|
+
<div class="studio-tl-camera-track" data-tl-area="camera">
|
|
4410
|
+
${focusBars || '<span class="studio-tl-camera-empty">focus \uC5C6\uC74C \xB7 \uFF0B \uB85C \uCD94\uAC00</span>'}
|
|
4411
|
+
</div>
|
|
4412
|
+
</div>
|
|
4413
|
+
</div>`;
|
|
4414
|
+
const trackRows = camera.tracks.map((track) => {
|
|
4415
|
+
const marks = track.keyframes.map(
|
|
4416
|
+
(kf) => `<div class="studio-tl-keyframe studio-tl-camera-keyframe" style="left:${timeToPx(kf.time)}px" data-camera-kf-prop="${track.property}" data-camera-kf-time="${kf.time}" title="${track.property} = ${kf.value} @ ${kf.time}ms (\uB4DC\uB798\uADF8\uB85C \uC774\uB3D9)">\u25C6</div>`
|
|
4417
|
+
).join("");
|
|
4418
|
+
return `
|
|
4419
|
+
<div class="studio-tl-row studio-tl-camera-row">
|
|
4420
|
+
<div class="studio-tl-gutter studio-tl-camera-gutter is-sub" data-camera-select>\u21B3 ${track.property}</div>
|
|
4421
|
+
<div class="studio-tl-body" style="width:${totalPx}px">
|
|
4422
|
+
<div class="studio-tl-camera-track" data-tl-area="camera">${marks}</div>
|
|
4423
|
+
</div>
|
|
4424
|
+
</div>`;
|
|
4425
|
+
}).join("");
|
|
4426
|
+
return focusRow + trackRows;
|
|
4427
|
+
}
|
|
3628
4428
|
function gutterLabel(el) {
|
|
3629
4429
|
return `${friendlyElementLabel(el)} \xB7 ${el.type}`;
|
|
3630
4430
|
}
|
|
@@ -3680,6 +4480,32 @@ function renderElementTracks(elements, currentTime, totalPx, sel) {
|
|
|
3680
4480
|
}
|
|
3681
4481
|
function onTracksClick(e) {
|
|
3682
4482
|
const target = e.target;
|
|
4483
|
+
if (target.closest("[data-add-camera-focus]")) {
|
|
4484
|
+
const time = getCurrentTime();
|
|
4485
|
+
const selected = getSelection();
|
|
4486
|
+
const elementIds = selected.kind === "element" ? [selected.elementId] : selected.kind === "elements" ? [...selected.elementIds] : (getDef()?.elements[0]?.id ?? "").length > 0 ? [getDef().elements[0].id] : [];
|
|
4487
|
+
if (elementIds.length === 0) return;
|
|
4488
|
+
addCameraFocus({
|
|
4489
|
+
time,
|
|
4490
|
+
duration: 600,
|
|
4491
|
+
elementIds,
|
|
4492
|
+
padding: 24,
|
|
4493
|
+
maxZoom: 4
|
|
4494
|
+
});
|
|
4495
|
+
return;
|
|
4496
|
+
}
|
|
4497
|
+
const focusBar = target.closest("[data-camera-focus]");
|
|
4498
|
+
if (focusBar) {
|
|
4499
|
+
setSelection({
|
|
4500
|
+
kind: "camera",
|
|
4501
|
+
focusIndex: Number(focusBar.dataset.cameraFocus)
|
|
4502
|
+
});
|
|
4503
|
+
return;
|
|
4504
|
+
}
|
|
4505
|
+
if (target.closest("[data-camera-select]")) {
|
|
4506
|
+
setSelection({ kind: "camera" });
|
|
4507
|
+
return;
|
|
4508
|
+
}
|
|
3683
4509
|
const chapterMarker = target.closest("[data-chapter-id]");
|
|
3684
4510
|
if (chapterMarker) {
|
|
3685
4511
|
setSelection({
|
|
@@ -3769,6 +4595,36 @@ function onMouseDown2(e) {
|
|
|
3769
4595
|
}
|
|
3770
4596
|
return;
|
|
3771
4597
|
}
|
|
4598
|
+
const cameraKf = target.closest("[data-camera-kf-prop]");
|
|
4599
|
+
if (cameraKf) {
|
|
4600
|
+
e.preventDefault();
|
|
4601
|
+
e.stopPropagation();
|
|
4602
|
+
const startTime = Number(cameraKf.dataset.cameraKfTime);
|
|
4603
|
+
dragMode = {
|
|
4604
|
+
kind: "camera-keyframe",
|
|
4605
|
+
prop: cameraKf.dataset.cameraKfProp,
|
|
4606
|
+
startTime,
|
|
4607
|
+
startMouseX: e.clientX,
|
|
4608
|
+
currentTime: startTime
|
|
4609
|
+
};
|
|
4610
|
+
return;
|
|
4611
|
+
}
|
|
4612
|
+
const cameraFocus = target.closest("[data-camera-focus]");
|
|
4613
|
+
if (cameraFocus) {
|
|
4614
|
+
e.preventDefault();
|
|
4615
|
+
e.stopPropagation();
|
|
4616
|
+
const index = Number(cameraFocus.dataset.cameraFocus);
|
|
4617
|
+
const entry = getCamera().focus[index];
|
|
4618
|
+
if (entry) {
|
|
4619
|
+
dragMode = {
|
|
4620
|
+
kind: "camera-focus",
|
|
4621
|
+
index,
|
|
4622
|
+
startTime: entry.time,
|
|
4623
|
+
startMouseX: e.clientX
|
|
4624
|
+
};
|
|
4625
|
+
}
|
|
4626
|
+
return;
|
|
4627
|
+
}
|
|
3772
4628
|
const chapterMarker = target.closest("[data-chapter-id]");
|
|
3773
4629
|
if (chapterMarker) {
|
|
3774
4630
|
e.preventDefault();
|
|
@@ -3829,6 +4685,29 @@ function onMouseMove2(e) {
|
|
|
3829
4685
|
showDragTooltip(e.clientX, e.clientY, `\u25C6 ${dragMode.prop} @ ${newTime} ms`);
|
|
3830
4686
|
return;
|
|
3831
4687
|
}
|
|
4688
|
+
if (dragMode.kind === "camera-keyframe") {
|
|
4689
|
+
const dt = Math.round((e.clientX - dragMode.startMouseX) / pxPerMs);
|
|
4690
|
+
const newTime = Math.max(0, dragMode.startTime + dt);
|
|
4691
|
+
if (newTime !== dragMode.currentTime) {
|
|
4692
|
+
moveCameraKeyframe(dragMode.prop, dragMode.currentTime, newTime);
|
|
4693
|
+
dragMode.currentTime = newTime;
|
|
4694
|
+
}
|
|
4695
|
+
showDragTooltip(
|
|
4696
|
+
e.clientX,
|
|
4697
|
+
e.clientY,
|
|
4698
|
+
`\u{1F3A5} ${dragMode.prop} @ ${newTime} ms`
|
|
4699
|
+
);
|
|
4700
|
+
return;
|
|
4701
|
+
}
|
|
4702
|
+
if (dragMode.kind === "camera-focus") {
|
|
4703
|
+
const dt = Math.round((e.clientX - dragMode.startMouseX) / pxPerMs);
|
|
4704
|
+
const newTime = Math.max(0, dragMode.startTime + dt);
|
|
4705
|
+
updateCameraFocus(dragMode.index, { time: newTime });
|
|
4706
|
+
const moved = getCamera().focus.findIndex((f) => f.time === newTime);
|
|
4707
|
+
if (moved >= 0) dragMode.index = moved;
|
|
4708
|
+
showDragTooltip(e.clientX, e.clientY, `\u{1F3A5} focus @ ${newTime} ms`);
|
|
4709
|
+
return;
|
|
4710
|
+
}
|
|
3832
4711
|
if (dragMode.kind === "appearance") {
|
|
3833
4712
|
const dxPx = e.clientX - dragMode.startMouseX;
|
|
3834
4713
|
const dt = Math.round(dxPx / pxPerMs);
|
|
@@ -15348,6 +16227,7 @@ var IconLibraryDialog = class _IconLibraryDialog {
|
|
|
15348
16227
|
rotation: 0,
|
|
15349
16228
|
appearances: [],
|
|
15350
16229
|
tracks: [],
|
|
16230
|
+
bindings: [],
|
|
15351
16231
|
x: cx - 32,
|
|
15352
16232
|
y: cy - 32,
|
|
15353
16233
|
width: 64,
|
|
@@ -15408,6 +16288,7 @@ async function uploadAndInsertImage(file, host) {
|
|
|
15408
16288
|
rotation: 0,
|
|
15409
16289
|
appearances: [],
|
|
15410
16290
|
tracks: [],
|
|
16291
|
+
bindings: [],
|
|
15411
16292
|
x: Math.round(cx - w / 2),
|
|
15412
16293
|
y: Math.round(cy - h / 2),
|
|
15413
16294
|
width: w,
|
|
@@ -15829,8 +16710,11 @@ var KIND_ICON = {
|
|
|
15829
16710
|
appearance: "\u25D0",
|
|
15830
16711
|
chapter: "\u{1F4CD}",
|
|
15831
16712
|
effect: "\u2728",
|
|
16713
|
+
camera: "\u{1F3A5}",
|
|
15832
16714
|
asset: "\u{1F5BC}",
|
|
15833
16715
|
group: "\u2B1A",
|
|
16716
|
+
layout: "\u25A6",
|
|
16717
|
+
checkpoint: "\u25C6",
|
|
15834
16718
|
other: "\xB7"
|
|
15835
16719
|
};
|
|
15836
16720
|
var dialogEl2 = null;
|
|
@@ -15951,7 +16835,8 @@ function queryUi() {
|
|
|
15951
16835
|
const undoBtn = $("studio-undo");
|
|
15952
16836
|
const redoBtn = $("studio-redo");
|
|
15953
16837
|
const gridToggleBtn = $("studio-grid-toggle");
|
|
15954
|
-
|
|
16838
|
+
const cameraFrameBtn = $("studio-camera-frame-toggle");
|
|
16839
|
+
if (!titleInput || !idDisplay || !status || !saveBtn || !exportBtn || !importBtn || !importFileInput || !deleteBtn || !newBtn || !openBtn || !playBtn || !restartBtn || !speedInput || !speedValue || !previewLoopInput || !detachTimelineBtn || !canvas || !elementList || !toolsRoot || !propsRoot || !timelineTracks || !elementTracks || !addStepBtn || !libraryDialog || !libraryList || !newDialog || !newIdInput || !newTitleInput || !newCreateBtn || !newError || !canvasWidthInput || !canvasHeightInput || !imageUploadBtn || !imageFileInput || !helpBtn || !helpDialog || !undoBtn || !redoBtn || !gridToggleBtn || !cameraFrameBtn) {
|
|
15955
16840
|
return null;
|
|
15956
16841
|
}
|
|
15957
16842
|
return {
|
|
@@ -15994,7 +16879,8 @@ function queryUi() {
|
|
|
15994
16879
|
helpDialog,
|
|
15995
16880
|
undoBtn,
|
|
15996
16881
|
redoBtn,
|
|
15997
|
-
gridToggleBtn
|
|
16882
|
+
gridToggleBtn,
|
|
16883
|
+
cameraFrameBtn
|
|
15998
16884
|
};
|
|
15999
16885
|
}
|
|
16000
16886
|
|
|
@@ -16102,6 +16988,14 @@ function reflectGridUi(ui) {
|
|
|
16102
16988
|
const label = document.getElementById("studio-grid-label");
|
|
16103
16989
|
if (label) label.textContent = on ? `\uACA9\uC790 ${size}px` : "\uACA9\uC790 \uB054";
|
|
16104
16990
|
}
|
|
16991
|
+
function reflectCameraFrameUi(ui) {
|
|
16992
|
+
const on = isCameraFrameVisible();
|
|
16993
|
+
ui.cameraFrameBtn.setAttribute("aria-pressed", on ? "true" : "false");
|
|
16994
|
+
ui.cameraFrameBtn.classList.toggle("is-active", on);
|
|
16995
|
+
ui.cameraFrameBtn.title = on ? "\uCE74\uBA54\uB77C \uC601\uC5ED \uD45C\uC2DC \uC911 \u2014 \uCE94\uBC84\uC2A4\uC5D0 \uD604\uC7AC \uC2DC\uAC01\uC758 \uCE74\uBA54\uB77C \uC0AC\uAC01\uD615\uC744 \uADF8\uB9BD\uB2C8\uB2E4" : "\uCE74\uBA54\uB77C \uC601\uC5ED \uD45C\uC2DC\uD558\uAE30";
|
|
16996
|
+
const label = document.getElementById("studio-camera-frame-label");
|
|
16997
|
+
if (label) label.textContent = on ? "\uCE74\uBA54\uB77C \uC601\uC5ED" : "\uCE74\uBA54\uB77C \uC601\uC5ED \uB054";
|
|
16998
|
+
}
|
|
16105
16999
|
function setupTimelineResizer(ui) {
|
|
16106
17000
|
const resizer = document.getElementById("studio-timeline-resizer");
|
|
16107
17001
|
if (!resizer) return;
|
|
@@ -16766,7 +17660,8 @@ function initStudio(opts = {}) {
|
|
|
16766
17660
|
const ui = queryUi();
|
|
16767
17661
|
if (!ui) {
|
|
16768
17662
|
console.error("[studio] missing required elements");
|
|
16769
|
-
return
|
|
17663
|
+
return () => {
|
|
17664
|
+
};
|
|
16770
17665
|
}
|
|
16771
17666
|
document.body.classList.add("editor-active");
|
|
16772
17667
|
document.documentElement.classList.add("editor-active");
|
|
@@ -16776,6 +17671,7 @@ function initStudio(opts = {}) {
|
|
|
16776
17671
|
initGrid();
|
|
16777
17672
|
reflectGridUi(ui);
|
|
16778
17673
|
subscribeGrid(() => reflectGridUi(ui));
|
|
17674
|
+
reflectCameraFrameUi(ui);
|
|
16779
17675
|
initCanvas(ui.canvas);
|
|
16780
17676
|
initElementList(ui.elementList, ui.toolsRoot);
|
|
16781
17677
|
initProperties(ui.propsRoot);
|
|
@@ -16856,6 +17752,18 @@ function initStudio(opts = {}) {
|
|
|
16856
17752
|
ui.newBtn.addEventListener("click", () => openNewDialog(ui));
|
|
16857
17753
|
initPalette();
|
|
16858
17754
|
initHistoryPanel();
|
|
17755
|
+
if (!getDef()) startDraft();
|
|
17756
|
+
const pluginMount = mountEditorPlugins(
|
|
17757
|
+
ui.app,
|
|
17758
|
+
opts.plugins ?? [],
|
|
17759
|
+
opts.resolvePluginPermissions ?? (() => ({})),
|
|
17760
|
+
{
|
|
17761
|
+
getDocument: getDef,
|
|
17762
|
+
replaceDocument: setDef,
|
|
17763
|
+
getSelection,
|
|
17764
|
+
setSelection
|
|
17765
|
+
}
|
|
17766
|
+
);
|
|
16859
17767
|
registerCommands([
|
|
16860
17768
|
{
|
|
16861
17769
|
id: "open",
|
|
@@ -16901,7 +17809,8 @@ function initStudio(opts = {}) {
|
|
|
16901
17809
|
hint: "G",
|
|
16902
17810
|
run: () => setGridEnabled(!isGridEnabled())
|
|
16903
17811
|
},
|
|
16904
|
-
{ id: "help", label: "\u2328 \uB2E8\uCD95\uD0A4 \uBCF4\uAE30", hint: "? / Shift+/", run: openHelp }
|
|
17812
|
+
{ id: "help", label: "\u2328 \uB2E8\uCD95\uD0A4 \uBCF4\uAE30", hint: "? / Shift+/", run: openHelp },
|
|
17813
|
+
...pluginMount.commands
|
|
16905
17814
|
]);
|
|
16906
17815
|
ui.saveBtn.addEventListener("click", () => void saveCurrent(ui));
|
|
16907
17816
|
ui.exportBtn.addEventListener("click", () => {
|
|
@@ -16915,7 +17824,9 @@ function initStudio(opts = {}) {
|
|
|
16915
17824
|
const file = ui.importFileInput.files?.[0];
|
|
16916
17825
|
ui.importFileInput.value = "";
|
|
16917
17826
|
if (!file) return;
|
|
16918
|
-
void file.text().then((text) =>
|
|
17827
|
+
void file.text().then((text) => JSON.parse(text)).then(
|
|
17828
|
+
(input) => opts.importDocument ? opts.importDocument(input) : animationDocumentSchema.parse(input)
|
|
17829
|
+
).then((def) => importAnimation(def)).then((def) => {
|
|
16919
17830
|
setDef(def);
|
|
16920
17831
|
markClean();
|
|
16921
17832
|
setStatus(ui, `${def.id}.json \uD30C\uC77C\uC744 \uAC00\uC838\uC654\uC2B5\uB2C8\uB2E4.`, "ok");
|
|
@@ -16934,6 +17845,10 @@ function initStudio(opts = {}) {
|
|
|
16934
17845
|
"click",
|
|
16935
17846
|
() => setGridEnabled(!isGridEnabled())
|
|
16936
17847
|
);
|
|
17848
|
+
ui.cameraFrameBtn.addEventListener("click", () => {
|
|
17849
|
+
setCameraFrameVisible(!isCameraFrameVisible());
|
|
17850
|
+
reflectCameraFrameUi(ui);
|
|
17851
|
+
});
|
|
16937
17852
|
ui.playBtn.addEventListener("click", () => togglePlay(ui));
|
|
16938
17853
|
ui.restartBtn.addEventListener("click", () => {
|
|
16939
17854
|
if (isPlaying()) {
|
|
@@ -16978,12 +17893,12 @@ function initStudio(opts = {}) {
|
|
|
16978
17893
|
});
|
|
16979
17894
|
if (opts.initialId) {
|
|
16980
17895
|
void loadAnimation2(ui, opts.initialId);
|
|
16981
|
-
} else if (
|
|
16982
|
-
startDraft();
|
|
17896
|
+
} else if (isDraft()) {
|
|
16983
17897
|
setStatus(ui, "\uC784\uC2DC \uC791\uC5C5 (Draft), \uC800\uC7A5 \uC2DC ID/\uC81C\uBAA9 \uC785\uB825", "ok");
|
|
16984
17898
|
} else {
|
|
16985
17899
|
setStatus(ui, "\uC900\uBE44\uB428", "ok");
|
|
16986
17900
|
}
|
|
17901
|
+
return () => pluginMount.dispose();
|
|
16987
17902
|
}
|
|
16988
17903
|
function reflectState(ui) {
|
|
16989
17904
|
ui.undoBtn.disabled = !canUndo();
|
|
@@ -17024,5 +17939,5 @@ function reflectState(ui) {
|
|
|
17024
17939
|
}
|
|
17025
17940
|
|
|
17026
17941
|
export { getVisibleElementIds, initStudio };
|
|
17027
|
-
//# sourceMappingURL=main-
|
|
17028
|
-
//# sourceMappingURL=main-
|
|
17942
|
+
//# sourceMappingURL=main-DC6D6RNQ.js.map
|
|
17943
|
+
//# sourceMappingURL=main-DC6D6RNQ.js.map
|