@kokoa/clotho-editor 0.2.0 → 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/dist/{chunk-FAI2G3XK.js → chunk-N47HF4VB.js} +177 -3
- package/dist/chunk-N47HF4VB.js.map +1 -0
- package/dist/clotho-editor.css +296 -0
- package/dist/index.d.ts +53 -3
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/{main-SKM3P4X7.js → main-DC6D6RNQ.js} +574 -7
- package/dist/{main-SKM3P4X7.js.map → main-DC6D6RNQ.js.map} +1 -1
- package/package.json +3 -3
- package/dist/chunk-FAI2G3XK.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
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, uniqueEffectId, addEffect, removeAppearance, removeTrack, removeTrackKeyframe, setTrackKeyframe,
|
|
2
|
-
import { activeAppearance, animationDocumentSchema, bindablePropertiesFor, resolveAsset, compileDataBindings } 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) {
|
|
@@ -1154,11 +1183,29 @@ var endpointDragState = null;
|
|
|
1154
1183
|
var vertexDragState = null;
|
|
1155
1184
|
var resizeState = null;
|
|
1156
1185
|
var marqueeState = null;
|
|
1186
|
+
var cameraDragState = null;
|
|
1157
1187
|
var marqueeJustFinished = false;
|
|
1158
1188
|
var toolJustFinished = false;
|
|
1159
1189
|
var canvasEl = null;
|
|
1160
1190
|
var hoveredElementId = null;
|
|
1161
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
|
+
}
|
|
1162
1209
|
var toolDrawState = null;
|
|
1163
1210
|
var elementDrawState = null;
|
|
1164
1211
|
var canvasRenderFrame = null;
|
|
@@ -1291,7 +1338,7 @@ function onCanvasClick(e) {
|
|
|
1291
1338
|
return;
|
|
1292
1339
|
}
|
|
1293
1340
|
if (target?.closest(
|
|
1294
|
-
"[data-rotate-handle], [data-anchor-handle], [data-resize-handle]"
|
|
1341
|
+
"[data-rotate-handle], [data-anchor-handle], [data-resize-handle], [data-camera-frame]"
|
|
1295
1342
|
))
|
|
1296
1343
|
return;
|
|
1297
1344
|
const id = findElementId(e.target);
|
|
@@ -1304,6 +1351,8 @@ function onCanvasClick(e) {
|
|
|
1304
1351
|
function onMouseDown(e) {
|
|
1305
1352
|
if (e.button !== 0) return;
|
|
1306
1353
|
const target = e.target;
|
|
1354
|
+
const cameraPart = target?.closest("[data-camera-frame]");
|
|
1355
|
+
if (cameraPart && beginCameraDrag(e, cameraPart)) return;
|
|
1307
1356
|
const def = getDef();
|
|
1308
1357
|
if (!def) return;
|
|
1309
1358
|
const activeTool2 = getActiveTool();
|
|
@@ -1568,6 +1617,11 @@ function collectDragExtras(anchorId, def, snap2) {
|
|
|
1568
1617
|
return extras;
|
|
1569
1618
|
}
|
|
1570
1619
|
function onMouseMove(e) {
|
|
1620
|
+
if (cameraDragState) {
|
|
1621
|
+
e.preventDefault();
|
|
1622
|
+
moveCameraDrag(e);
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1571
1625
|
if (pathDraftState && getActiveTool() === "path") {
|
|
1572
1626
|
const point = svgPoint(e.clientX, e.clientY);
|
|
1573
1627
|
if (point) {
|
|
@@ -1768,6 +1822,11 @@ function finishPathDraft() {
|
|
|
1768
1822
|
render2();
|
|
1769
1823
|
}
|
|
1770
1824
|
function onMouseUp(e) {
|
|
1825
|
+
if (cameraDragState) {
|
|
1826
|
+
cameraDragState = null;
|
|
1827
|
+
endTransient();
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1771
1830
|
if (elementDrawState) {
|
|
1772
1831
|
const draw = elementDrawState;
|
|
1773
1832
|
const point = svgPoint(e.clientX, e.clientY);
|
|
@@ -2176,6 +2235,8 @@ function render2() {
|
|
|
2176
2235
|
canvasEl.appendChild(g);
|
|
2177
2236
|
}
|
|
2178
2237
|
}
|
|
2238
|
+
const cameraFrame = renderCameraFrame(def);
|
|
2239
|
+
if (cameraFrame) canvasEl.appendChild(cameraFrame);
|
|
2179
2240
|
const selection = getSelection();
|
|
2180
2241
|
if (selection.kind === "element") {
|
|
2181
2242
|
const selEl = elementsById.get(selection.elementId);
|
|
@@ -2386,6 +2447,141 @@ function renderGroupOutline(groupId) {
|
|
|
2386
2447
|
`;
|
|
2387
2448
|
return g;
|
|
2388
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
|
+
}
|
|
2389
2585
|
function makeG(elementId, rotation, cx, cy) {
|
|
2390
2586
|
const g = document.createElementNS(SVG_NS, "g");
|
|
2391
2587
|
g.setAttribute("data-elem-id", elementId);
|
|
@@ -2762,8 +2958,23 @@ function initProperties(root) {
|
|
|
2762
2958
|
root.addEventListener("compositionend", onCompositionEnd);
|
|
2763
2959
|
root.addEventListener("toggle", onSectionToggle, true);
|
|
2764
2960
|
root.addEventListener("wheel", onNumberWheel, { passive: false });
|
|
2961
|
+
root.addEventListener("pointerdown", onSliderGrab);
|
|
2962
|
+
root.addEventListener("pointerup", onSliderRelease);
|
|
2963
|
+
root.addEventListener("pointercancel", onSliderRelease);
|
|
2765
2964
|
render3();
|
|
2766
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
|
+
}
|
|
2767
2978
|
function onCompositionStart() {
|
|
2768
2979
|
isComposingFallback = true;
|
|
2769
2980
|
}
|
|
@@ -2984,6 +3195,8 @@ function renderInner() {
|
|
|
2984
3195
|
</select>
|
|
2985
3196
|
<button type="button" class="studio-btn" data-add-effect>\uFF0B \uD6A8\uACFC</button>
|
|
2986
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>
|
|
2987
3200
|
`;
|
|
2988
3201
|
return;
|
|
2989
3202
|
}
|
|
@@ -3042,6 +3255,10 @@ function renderInner() {
|
|
|
3042
3255
|
renderElementForm(def, el);
|
|
3043
3256
|
return;
|
|
3044
3257
|
}
|
|
3258
|
+
if (sel.kind === "camera") {
|
|
3259
|
+
renderCameraForm(def, sel.focusIndex);
|
|
3260
|
+
return;
|
|
3261
|
+
}
|
|
3045
3262
|
if (sel.kind === "chapter") {
|
|
3046
3263
|
const ch = def.chapters.find((c) => c.id === sel.chapterId);
|
|
3047
3264
|
if (!ch) {
|
|
@@ -3086,6 +3303,114 @@ function renderInner() {
|
|
|
3086
3303
|
return;
|
|
3087
3304
|
}
|
|
3088
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
|
+
}
|
|
3089
3414
|
function renderElementForm(def, el) {
|
|
3090
3415
|
if (!panelEl) return;
|
|
3091
3416
|
const timeHint = `<span class="studio-step-hint">\u{1F4CD} t = ${getCurrentTime()} ms</span>`;
|
|
@@ -3367,6 +3692,11 @@ function renderTracks(el) {
|
|
|
3367
3692
|
}
|
|
3368
3693
|
function onInput(e) {
|
|
3369
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
|
+
}
|
|
3370
3700
|
const key = target.dataset.propKey;
|
|
3371
3701
|
if (!key) return;
|
|
3372
3702
|
if (target.type === "text" && e.isComposing) return;
|
|
@@ -3385,6 +3715,16 @@ function onInput(e) {
|
|
|
3385
3715
|
value = target.checked;
|
|
3386
3716
|
apply(key, value);
|
|
3387
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
|
+
}
|
|
3388
3728
|
function onChange(e) {
|
|
3389
3729
|
const target = e.target;
|
|
3390
3730
|
if (target instanceof HTMLInputElement && target.type === "color") {
|
|
@@ -3485,6 +3825,33 @@ function apply(key, value) {
|
|
|
3485
3825
|
predicate: { type: "equals", value: answer }
|
|
3486
3826
|
});
|
|
3487
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)) });
|
|
3488
3855
|
} else if (key.startsWith("el.") && sel.kind === "element") {
|
|
3489
3856
|
const prop = key.slice(3);
|
|
3490
3857
|
const time = getCurrentTime();
|
|
@@ -3682,6 +4049,74 @@ function onClick2(e) {
|
|
|
3682
4049
|
});
|
|
3683
4050
|
return;
|
|
3684
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
|
+
}
|
|
3685
4120
|
if (target.closest("[data-add-chapter]")) {
|
|
3686
4121
|
const id = uniqueChapterId();
|
|
3687
4122
|
addChapter({
|
|
@@ -3930,6 +4365,7 @@ function render4() {
|
|
|
3930
4365
|
<div class="studio-tl-chapter-label">${escapeHtml3(c.label || c.id)}</div>
|
|
3931
4366
|
</div>`;
|
|
3932
4367
|
}).join("");
|
|
4368
|
+
const cameraRows = renderCameraRows(totalPx, sel);
|
|
3933
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>`;
|
|
3934
4370
|
tracksEl.innerHTML = `
|
|
3935
4371
|
<div class="studio-tl-chart">
|
|
@@ -3947,12 +4383,48 @@ function render4() {
|
|
|
3947
4383
|
</div>
|
|
3948
4384
|
</div>
|
|
3949
4385
|
</div>
|
|
4386
|
+
${cameraRows}
|
|
3950
4387
|
${playheadCol}
|
|
3951
4388
|
</div>
|
|
3952
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>
|
|
3953
4390
|
`;
|
|
3954
4391
|
renderElementTracks(def.elements, currentTime, totalPx, sel);
|
|
3955
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
|
+
}
|
|
3956
4428
|
function gutterLabel(el) {
|
|
3957
4429
|
return `${friendlyElementLabel(el)} \xB7 ${el.type}`;
|
|
3958
4430
|
}
|
|
@@ -4008,6 +4480,32 @@ function renderElementTracks(elements, currentTime, totalPx, sel) {
|
|
|
4008
4480
|
}
|
|
4009
4481
|
function onTracksClick(e) {
|
|
4010
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
|
+
}
|
|
4011
4509
|
const chapterMarker = target.closest("[data-chapter-id]");
|
|
4012
4510
|
if (chapterMarker) {
|
|
4013
4511
|
setSelection({
|
|
@@ -4097,6 +4595,36 @@ function onMouseDown2(e) {
|
|
|
4097
4595
|
}
|
|
4098
4596
|
return;
|
|
4099
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
|
+
}
|
|
4100
4628
|
const chapterMarker = target.closest("[data-chapter-id]");
|
|
4101
4629
|
if (chapterMarker) {
|
|
4102
4630
|
e.preventDefault();
|
|
@@ -4157,6 +4685,29 @@ function onMouseMove2(e) {
|
|
|
4157
4685
|
showDragTooltip(e.clientX, e.clientY, `\u25C6 ${dragMode.prop} @ ${newTime} ms`);
|
|
4158
4686
|
return;
|
|
4159
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
|
+
}
|
|
4160
4711
|
if (dragMode.kind === "appearance") {
|
|
4161
4712
|
const dxPx = e.clientX - dragMode.startMouseX;
|
|
4162
4713
|
const dt = Math.round(dxPx / pxPerMs);
|
|
@@ -16159,6 +16710,7 @@ var KIND_ICON = {
|
|
|
16159
16710
|
appearance: "\u25D0",
|
|
16160
16711
|
chapter: "\u{1F4CD}",
|
|
16161
16712
|
effect: "\u2728",
|
|
16713
|
+
camera: "\u{1F3A5}",
|
|
16162
16714
|
asset: "\u{1F5BC}",
|
|
16163
16715
|
group: "\u2B1A",
|
|
16164
16716
|
layout: "\u25A6",
|
|
@@ -16283,7 +16835,8 @@ function queryUi() {
|
|
|
16283
16835
|
const undoBtn = $("studio-undo");
|
|
16284
16836
|
const redoBtn = $("studio-redo");
|
|
16285
16837
|
const gridToggleBtn = $("studio-grid-toggle");
|
|
16286
|
-
|
|
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) {
|
|
16287
16840
|
return null;
|
|
16288
16841
|
}
|
|
16289
16842
|
return {
|
|
@@ -16326,7 +16879,8 @@ function queryUi() {
|
|
|
16326
16879
|
helpDialog,
|
|
16327
16880
|
undoBtn,
|
|
16328
16881
|
redoBtn,
|
|
16329
|
-
gridToggleBtn
|
|
16882
|
+
gridToggleBtn,
|
|
16883
|
+
cameraFrameBtn
|
|
16330
16884
|
};
|
|
16331
16885
|
}
|
|
16332
16886
|
|
|
@@ -16434,6 +16988,14 @@ function reflectGridUi(ui) {
|
|
|
16434
16988
|
const label = document.getElementById("studio-grid-label");
|
|
16435
16989
|
if (label) label.textContent = on ? `\uACA9\uC790 ${size}px` : "\uACA9\uC790 \uB054";
|
|
16436
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
|
+
}
|
|
16437
16999
|
function setupTimelineResizer(ui) {
|
|
16438
17000
|
const resizer = document.getElementById("studio-timeline-resizer");
|
|
16439
17001
|
if (!resizer) return;
|
|
@@ -17109,6 +17671,7 @@ function initStudio(opts = {}) {
|
|
|
17109
17671
|
initGrid();
|
|
17110
17672
|
reflectGridUi(ui);
|
|
17111
17673
|
subscribeGrid(() => reflectGridUi(ui));
|
|
17674
|
+
reflectCameraFrameUi(ui);
|
|
17112
17675
|
initCanvas(ui.canvas);
|
|
17113
17676
|
initElementList(ui.elementList, ui.toolsRoot);
|
|
17114
17677
|
initProperties(ui.propsRoot);
|
|
@@ -17282,6 +17845,10 @@ function initStudio(opts = {}) {
|
|
|
17282
17845
|
"click",
|
|
17283
17846
|
() => setGridEnabled(!isGridEnabled())
|
|
17284
17847
|
);
|
|
17848
|
+
ui.cameraFrameBtn.addEventListener("click", () => {
|
|
17849
|
+
setCameraFrameVisible(!isCameraFrameVisible());
|
|
17850
|
+
reflectCameraFrameUi(ui);
|
|
17851
|
+
});
|
|
17285
17852
|
ui.playBtn.addEventListener("click", () => togglePlay(ui));
|
|
17286
17853
|
ui.restartBtn.addEventListener("click", () => {
|
|
17287
17854
|
if (isPlaying()) {
|
|
@@ -17372,5 +17939,5 @@ function reflectState(ui) {
|
|
|
17372
17939
|
}
|
|
17373
17940
|
|
|
17374
17941
|
export { getVisibleElementIds, initStudio };
|
|
17375
|
-
//# sourceMappingURL=main-
|
|
17376
|
-
//# sourceMappingURL=main-
|
|
17942
|
+
//# sourceMappingURL=main-DC6D6RNQ.js.map
|
|
17943
|
+
//# sourceMappingURL=main-DC6D6RNQ.js.map
|