@kokoa/clotho-editor 0.2.0 → 0.4.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-WOP52KFW.js} +196 -4
- package/dist/chunk-WOP52KFW.js.map +1 -0
- package/dist/clotho-editor.css +301 -0
- package/dist/index.d.ts +62 -3
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/{main-SKM3P4X7.js → main-HDE5SDKM.js} +736 -12
- package/dist/main-HDE5SDKM.js.map +1 -0
- package/package.json +3 -3
- package/dist/chunk-FAI2G3XK.js.map +0 -1
- package/dist/main-SKM3P4X7.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, updateEffect, updateLocales, updateData, updateResponsive, updateDuration, updateCheckpoint, setCameraStrokeScaling, updateAppearance, moveCameraKeyframe, childIdsOf, replaceEffect } from './chunk-WOP52KFW.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,10 @@ function render2() {
|
|
|
2176
2235
|
canvasEl.appendChild(g);
|
|
2177
2236
|
}
|
|
2178
2237
|
}
|
|
2238
|
+
const cameraFrame = renderCameraFrame(def);
|
|
2239
|
+
if (cameraFrame) canvasEl.appendChild(cameraFrame);
|
|
2240
|
+
const spotlightPreview = renderSpotlightPreview(def, snap2, elementsById);
|
|
2241
|
+
if (spotlightPreview) canvasEl.appendChild(spotlightPreview);
|
|
2179
2242
|
const selection = getSelection();
|
|
2180
2243
|
if (selection.kind === "element") {
|
|
2181
2244
|
const selEl = elementsById.get(selection.elementId);
|
|
@@ -2386,6 +2449,199 @@ function renderGroupOutline(groupId) {
|
|
|
2386
2449
|
`;
|
|
2387
2450
|
return g;
|
|
2388
2451
|
}
|
|
2452
|
+
function renderCameraFrame(def) {
|
|
2453
|
+
if (!showCameraFrame) return null;
|
|
2454
|
+
const view = computeCamera(def, getCurrentTime());
|
|
2455
|
+
if (!view) return null;
|
|
2456
|
+
const { width, height } = def.canvas;
|
|
2457
|
+
const selected = getSelection().kind === "camera";
|
|
2458
|
+
const control = cameraControlAt(getCurrentTime());
|
|
2459
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
2460
|
+
g.setAttribute(
|
|
2461
|
+
"class",
|
|
2462
|
+
`studio-camera-frame${selected ? " is-selected" : ""}`
|
|
2463
|
+
);
|
|
2464
|
+
g.setAttribute("pointer-events", "none");
|
|
2465
|
+
const scrim = document.createElementNS(SVG_NS, "path");
|
|
2466
|
+
scrim.setAttribute(
|
|
2467
|
+
"d",
|
|
2468
|
+
`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`
|
|
2469
|
+
);
|
|
2470
|
+
scrim.setAttribute("fill-rule", "evenodd");
|
|
2471
|
+
scrim.setAttribute("class", "studio-camera-frame-scrim");
|
|
2472
|
+
g.appendChild(scrim);
|
|
2473
|
+
const rect = document.createElementNS(SVG_NS, "rect");
|
|
2474
|
+
rect.setAttribute("x", String(view.x));
|
|
2475
|
+
rect.setAttribute("y", String(view.y));
|
|
2476
|
+
rect.setAttribute("width", String(view.width));
|
|
2477
|
+
rect.setAttribute("height", String(view.height));
|
|
2478
|
+
rect.setAttribute("class", "studio-camera-frame-rect");
|
|
2479
|
+
rect.setAttribute("vector-effect", "non-scaling-stroke");
|
|
2480
|
+
rect.setAttribute("data-camera-frame", "border");
|
|
2481
|
+
rect.setAttribute("pointer-events", "stroke");
|
|
2482
|
+
g.appendChild(rect);
|
|
2483
|
+
if (selected && control.kind !== "none") {
|
|
2484
|
+
g.appendChild(cameraHandles(view, control));
|
|
2485
|
+
}
|
|
2486
|
+
const label = document.createElementNS(SVG_NS, "text");
|
|
2487
|
+
label.setAttribute("x", String(view.x + 6));
|
|
2488
|
+
label.setAttribute("y", String(view.y + 16));
|
|
2489
|
+
label.setAttribute("class", "studio-camera-frame-label");
|
|
2490
|
+
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)}`;
|
|
2491
|
+
g.appendChild(label);
|
|
2492
|
+
if (view.issues.length > 0) {
|
|
2493
|
+
const warning = document.createElementNS(SVG_NS, "text");
|
|
2494
|
+
warning.setAttribute("x", String(view.x + 6));
|
|
2495
|
+
warning.setAttribute("y", String(view.y + 32));
|
|
2496
|
+
warning.setAttribute("class", "studio-camera-frame-warning");
|
|
2497
|
+
warning.textContent = `\u26A0 ${view.issues[0].message}`;
|
|
2498
|
+
g.appendChild(warning);
|
|
2499
|
+
}
|
|
2500
|
+
return g;
|
|
2501
|
+
}
|
|
2502
|
+
function beginCameraDrag(e, part) {
|
|
2503
|
+
const def = getDef();
|
|
2504
|
+
if (!def) return false;
|
|
2505
|
+
const time = getCurrentTime();
|
|
2506
|
+
const view = computeCamera(def, time);
|
|
2507
|
+
if (!view) return false;
|
|
2508
|
+
const wasSelected = getSelection().kind === "camera";
|
|
2509
|
+
if (!wasSelected) setSelection({ kind: "camera" });
|
|
2510
|
+
e.preventDefault();
|
|
2511
|
+
e.stopPropagation();
|
|
2512
|
+
const kind = part.dataset.cameraFrame;
|
|
2513
|
+
if (kind === "border" || !wasSelected) return true;
|
|
2514
|
+
const control = cameraControlAt(time);
|
|
2515
|
+
const mode = cameraDragMode(kind, control);
|
|
2516
|
+
if (!mode) return true;
|
|
2517
|
+
const point = svgPoint(e.clientX, e.clientY);
|
|
2518
|
+
if (!point) return true;
|
|
2519
|
+
const focusIndex = control.kind === "focus" ? control.index : -1;
|
|
2520
|
+
const padding = focusIndex >= 0 ? getCamera().focus[focusIndex]?.padding ?? 24 : 0;
|
|
2521
|
+
cameraDragState = {
|
|
2522
|
+
mode,
|
|
2523
|
+
focusIndex,
|
|
2524
|
+
time,
|
|
2525
|
+
startX: point.x,
|
|
2526
|
+
startY: point.y,
|
|
2527
|
+
startCenterX: view.centerX,
|
|
2528
|
+
startCenterY: view.centerY,
|
|
2529
|
+
startZoom: view.zoom,
|
|
2530
|
+
startPadding: padding,
|
|
2531
|
+
startDistance: centreDistance(point, view.centerX, view.centerY)
|
|
2532
|
+
};
|
|
2533
|
+
beginTransient("\uCE74\uBA54\uB77C \uC870\uC815", "camera");
|
|
2534
|
+
return true;
|
|
2535
|
+
}
|
|
2536
|
+
function moveCameraDrag(e) {
|
|
2537
|
+
const drag = cameraDragState;
|
|
2538
|
+
if (!drag) return;
|
|
2539
|
+
const point = svgPoint(e.clientX, e.clientY);
|
|
2540
|
+
if (!point) return;
|
|
2541
|
+
const next = cameraDragResult(drag, point);
|
|
2542
|
+
if (next.centerX !== void 0) {
|
|
2543
|
+
setCameraKeyframe("x", drag.time, next.centerX);
|
|
2544
|
+
}
|
|
2545
|
+
if (next.centerY !== void 0) {
|
|
2546
|
+
setCameraKeyframe("y", drag.time, next.centerY);
|
|
2547
|
+
}
|
|
2548
|
+
if (next.zoom !== void 0) setCameraKeyframe("zoom", drag.time, next.zoom);
|
|
2549
|
+
if (next.padding !== void 0) {
|
|
2550
|
+
updateCameraFocus(drag.focusIndex, { padding: next.padding });
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
function renderSpotlightPreview(def, snap2, elementsById) {
|
|
2554
|
+
const selection = getSelection();
|
|
2555
|
+
if (selection.kind !== "effect") return null;
|
|
2556
|
+
const effect = def.effects.find(
|
|
2557
|
+
(candidate) => candidate.id === selection.effectId
|
|
2558
|
+
);
|
|
2559
|
+
if (!effect || effect.type !== "spotlight") return null;
|
|
2560
|
+
const boxes = effect.elementIds.map((id) => {
|
|
2561
|
+
const el = elementsById.get(id);
|
|
2562
|
+
const state = el ? snap2.get(id) : void 0;
|
|
2563
|
+
return el && state ? elementBBox(el, state) : null;
|
|
2564
|
+
}).filter(
|
|
2565
|
+
(box) => box !== null
|
|
2566
|
+
);
|
|
2567
|
+
if (boxes.length === 0) return null;
|
|
2568
|
+
const { width, height } = def.canvas;
|
|
2569
|
+
const pad = effect.padding;
|
|
2570
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
2571
|
+
g.setAttribute("class", "studio-spotlight-preview");
|
|
2572
|
+
g.setAttribute("pointer-events", "none");
|
|
2573
|
+
const holes = [];
|
|
2574
|
+
if (effect.shape === "elements") {
|
|
2575
|
+
for (const box of boxes) {
|
|
2576
|
+
holes.push(
|
|
2577
|
+
`M${box.x - pad} ${box.y - pad}H${box.x + box.w + pad}V${box.y + box.h + pad}H${box.x - pad}Z`
|
|
2578
|
+
);
|
|
2579
|
+
}
|
|
2580
|
+
} else {
|
|
2581
|
+
const x = Math.min(...boxes.map((box) => box.x)) - pad;
|
|
2582
|
+
const y = Math.min(...boxes.map((box) => box.y)) - pad;
|
|
2583
|
+
const right = Math.max(...boxes.map((box) => box.x + box.w)) + pad;
|
|
2584
|
+
const bottom = Math.max(...boxes.map((box) => box.y + box.h)) + pad;
|
|
2585
|
+
if (effect.shape === "circle") {
|
|
2586
|
+
const cx = (x + right) / 2;
|
|
2587
|
+
const cy = (y + bottom) / 2;
|
|
2588
|
+
const r = Math.hypot(right - x, bottom - y) / 2;
|
|
2589
|
+
holes.push(
|
|
2590
|
+
`M${cx - r} ${cy}a${r} ${r} 0 1 0 ${r * 2} 0a${r} ${r} 0 1 0 ${-r * 2} 0Z`
|
|
2591
|
+
);
|
|
2592
|
+
} else {
|
|
2593
|
+
holes.push(`M${x} ${y}H${right}V${bottom}H${x}Z`);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
const scrim = document.createElementNS(SVG_NS, "path");
|
|
2597
|
+
scrim.setAttribute("d", `M0 0H${width}V${height}H0Z ${holes.join(" ")}`);
|
|
2598
|
+
scrim.setAttribute("fill-rule", "evenodd");
|
|
2599
|
+
scrim.setAttribute("fill", effect.dimColor ?? "#0b1120");
|
|
2600
|
+
scrim.setAttribute("opacity", String(Math.max(effect.dim, 0.15)));
|
|
2601
|
+
g.appendChild(scrim);
|
|
2602
|
+
const label = document.createElementNS(SVG_NS, "text");
|
|
2603
|
+
label.setAttribute("x", "8");
|
|
2604
|
+
label.setAttribute("y", "18");
|
|
2605
|
+
label.setAttribute("class", "studio-spotlight-preview-label");
|
|
2606
|
+
const approximated = effect.shape === "elements" ? " \xB7 \uC2E4\uB8E8\uC5E3\uC740 \uC0C1\uC790\uB85C \uADFC\uC0AC" : "";
|
|
2607
|
+
label.textContent = `\u{1F526} ${effect.id} \xB7 ${effect.time}\u2013${effect.time + effect.duration}ms${approximated}`;
|
|
2608
|
+
g.appendChild(label);
|
|
2609
|
+
return g;
|
|
2610
|
+
}
|
|
2611
|
+
function cameraHandles(view, control) {
|
|
2612
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
2613
|
+
if (control.kind === "tracks") {
|
|
2614
|
+
const body = document.createElementNS(SVG_NS, "rect");
|
|
2615
|
+
body.setAttribute("x", String(view.x));
|
|
2616
|
+
body.setAttribute("y", String(view.y));
|
|
2617
|
+
body.setAttribute("width", String(view.width));
|
|
2618
|
+
body.setAttribute("height", String(view.height));
|
|
2619
|
+
body.setAttribute("class", "studio-camera-frame-body");
|
|
2620
|
+
body.setAttribute("data-camera-frame", "pan");
|
|
2621
|
+
body.setAttribute("pointer-events", "fill");
|
|
2622
|
+
g.appendChild(body);
|
|
2623
|
+
}
|
|
2624
|
+
const corners = [
|
|
2625
|
+
["nw", view.x, view.y],
|
|
2626
|
+
["ne", view.x + view.width, view.y],
|
|
2627
|
+
["sw", view.x, view.y + view.height],
|
|
2628
|
+
["se", view.x + view.width, view.y + view.height]
|
|
2629
|
+
];
|
|
2630
|
+
const size = 9 / canvasZoom;
|
|
2631
|
+
for (const [corner, cx, cy] of corners) {
|
|
2632
|
+
const handle = document.createElementNS(SVG_NS, "rect");
|
|
2633
|
+
handle.setAttribute("x", String(cx - size / 2));
|
|
2634
|
+
handle.setAttribute("y", String(cy - size / 2));
|
|
2635
|
+
handle.setAttribute("width", String(size));
|
|
2636
|
+
handle.setAttribute("height", String(size));
|
|
2637
|
+
handle.setAttribute("class", "studio-camera-frame-handle");
|
|
2638
|
+
handle.setAttribute("data-camera-frame", "resize");
|
|
2639
|
+
handle.setAttribute("data-camera-corner", corner);
|
|
2640
|
+
handle.setAttribute("pointer-events", "all");
|
|
2641
|
+
g.appendChild(handle);
|
|
2642
|
+
}
|
|
2643
|
+
return g;
|
|
2644
|
+
}
|
|
2389
2645
|
function makeG(elementId, rotation, cx, cy) {
|
|
2390
2646
|
const g = document.createElementNS(SVG_NS, "g");
|
|
2391
2647
|
g.setAttribute("data-elem-id", elementId);
|
|
@@ -2762,8 +3018,23 @@ function initProperties(root) {
|
|
|
2762
3018
|
root.addEventListener("compositionend", onCompositionEnd);
|
|
2763
3019
|
root.addEventListener("toggle", onSectionToggle, true);
|
|
2764
3020
|
root.addEventListener("wheel", onNumberWheel, { passive: false });
|
|
3021
|
+
root.addEventListener("pointerdown", onSliderGrab);
|
|
3022
|
+
root.addEventListener("pointerup", onSliderRelease);
|
|
3023
|
+
root.addEventListener("pointercancel", onSliderRelease);
|
|
2765
3024
|
render3();
|
|
2766
3025
|
}
|
|
3026
|
+
var slidingKey = null;
|
|
3027
|
+
function onSliderGrab(e) {
|
|
3028
|
+
const target = e.target;
|
|
3029
|
+
if (target.type !== "range" || !target.dataset.propKey) return;
|
|
3030
|
+
slidingKey = target.dataset.propKey;
|
|
3031
|
+
beginTransient(`\uC870\uC815: ${slidingKey}`, "camera");
|
|
3032
|
+
}
|
|
3033
|
+
function onSliderRelease() {
|
|
3034
|
+
if (slidingKey === null) return;
|
|
3035
|
+
slidingKey = null;
|
|
3036
|
+
endTransient();
|
|
3037
|
+
}
|
|
2767
3038
|
function onCompositionStart() {
|
|
2768
3039
|
isComposingFallback = true;
|
|
2769
3040
|
}
|
|
@@ -2981,9 +3252,12 @@ function renderInner() {
|
|
|
2981
3252
|
<option value="highlight">highlight</option>
|
|
2982
3253
|
<option value="pulse">pulse</option>
|
|
2983
3254
|
<option value="flow">flow</option>
|
|
3255
|
+
<option value="spotlight">spotlight</option>
|
|
2984
3256
|
</select>
|
|
2985
3257
|
<button type="button" class="studio-btn" data-add-effect>\uFF0B \uD6A8\uACFC</button>
|
|
2986
3258
|
</div>
|
|
3259
|
+
<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>
|
|
3260
|
+
<button type="button" class="studio-btn" data-open-camera>\u{1F3A5} \uCE74\uBA54\uB77C \uD3B8\uC9D1</button>
|
|
2987
3261
|
`;
|
|
2988
3262
|
return;
|
|
2989
3263
|
}
|
|
@@ -3042,6 +3316,10 @@ function renderInner() {
|
|
|
3042
3316
|
renderElementForm(def, el);
|
|
3043
3317
|
return;
|
|
3044
3318
|
}
|
|
3319
|
+
if (sel.kind === "camera") {
|
|
3320
|
+
renderCameraForm(def, sel.focusIndex);
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3045
3323
|
if (sel.kind === "chapter") {
|
|
3046
3324
|
const ch = def.chapters.find((c) => c.id === sel.chapterId);
|
|
3047
3325
|
if (!ch) {
|
|
@@ -3066,12 +3344,40 @@ function renderInner() {
|
|
|
3066
3344
|
setSelection({ kind: "none" });
|
|
3067
3345
|
return;
|
|
3068
3346
|
}
|
|
3347
|
+
const typeOpts = ["highlight", "pulse", "flow", "spotlight"].map(
|
|
3348
|
+
(t) => `<option value="${t}" ${t === eff.type ? "selected" : ""}>${t}</option>`
|
|
3349
|
+
).join("");
|
|
3350
|
+
if (eff.type === "spotlight") {
|
|
3351
|
+
const shapeOpts = ["bbox", "circle", "elements"].map(
|
|
3352
|
+
(shape) => `<option value="${shape}" ${shape === eff.shape ? "selected" : ""}>${shape}${shape === "bbox" ? " (\uAE30\uBCF8)" : ""}</option>`
|
|
3353
|
+
).join("");
|
|
3354
|
+
panelEl.innerHTML = `
|
|
3355
|
+
${timeHint}
|
|
3356
|
+
<div class="studio-props-header"><span class="studio-props-header-title">${escapeHtml2(eff.id)}</span><span class="studio-props-header-type">spotlight</span></div>
|
|
3357
|
+
<label class="studio-field"><span>type</span><select data-prop-key="effect.type">${typeOpts}</select></label>
|
|
3358
|
+
<p class="studio-props-empty" style="margin:0 0 0.4rem">\uB300\uC0C1\uC740 \uADF8\uB300\uB85C \uB450\uACE0 \uB098\uBA38\uC9C0\uB97C \uC5B4\uB461\uAC8C \uD569\uB2C8\uB2E4. \uB300\uC0C1\uC758 \uC0C9\uC774 \uC815\uBCF4\uC778 \uBB38\uC11C\uC5D0\uC11C <code>highlight</code> \uB300\uC2E0 \uC501\uB2C8\uB2E4.</p>
|
|
3359
|
+
${numberField("time (ms)", "effect.time", eff.time, 50)}
|
|
3360
|
+
${numberField("duration (ms)", "effect.duration", eff.duration, 50)}
|
|
3361
|
+
${numberField("fadeIn (ms) \xB7 \uC591 \uB05D\uC5D0 \uC801\uC6A9", "effect.fadeIn", eff.fadeIn, 50)}
|
|
3362
|
+
<div class="studio-camera-field-label">\uB300\uC0C1 \uC694\uC18C (${eff.elementIds.length})</div>
|
|
3363
|
+
${elementCheckList(def, "data-spotlight-target", eff.id, eff.elementIds)}
|
|
3364
|
+
<label class="studio-field"><span>shape</span><select data-prop-key="effect.shape">${shapeOpts}</select></label>
|
|
3365
|
+
<p class="studio-camera-hint">\uB300\uC0C1\uC774 \uBB34\uB300 \uC5EC\uAE30\uC800\uAE30 \uD769\uC5B4\uC838 \uC788\uC73C\uBA74 <code>elements</code>\uB97C \uC4F0\uC138\uC694. <code>bbox</code>\uB294 \uC14B\uC744 \uB2F4\uB290\uB77C \uADF8 \uC0AC\uC774\uC758 \uAD00\uACC4\uC5C6\uB294 \uC694\uC18C\uAE4C\uC9C0 \uBC1D\uD799\uB2C8\uB2E4.</p>
|
|
3366
|
+
${rangeField("padding", "effect.padding", eff.padding, 0, 80, 1)}
|
|
3367
|
+
<div class="studio-props-header" style="margin-top:0.6rem"><span class="studio-props-header-title">\uAC00\uB824\uC9C0\uB294 \uCABD</span></div>
|
|
3368
|
+
${rangeField("dim", "effect.dim", eff.dim, 0, 1, 0.01, "0\uC774\uBA74 \uC5B4\uB461\uAC8C \uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.")}
|
|
3369
|
+
${colorField("dimColor", "effect.dimColor", eff.dimColor ?? "#0b1120")}
|
|
3370
|
+
<p class="studio-camera-hint">\uBE44\uC6CC \uB450\uBA74 \uD14C\uB9C8 \uD1A0\uD070\uC744 \uC501\uB2C8\uB2E4 \u2014 \uB77C\uC774\uD2B8\xB7\uB2E4\uD06C \uBAA8\uB450 near-black.</p>
|
|
3371
|
+
<div class="studio-props-header" style="margin-top:0.6rem"><span class="studio-props-header-title">\uBE44\uCD94\uB294 \uCABD</span></div>
|
|
3372
|
+
${rangeField("lit", "effect.lit", eff.lit, 0, 1, 0.01, "0\uC774\uBA74 \uB300\uC0C1\uC740 \uC790\uAE30 \uC0C9 \uADF8\uB300\uB85C\uC785\uB2C8\uB2E4. \uC870\uBA85\uC758 \uC824\uCC98\uB7FC \uC0C9\uC744 \uC5B9\uC73C\uB824\uBA74 \uC62C\uB9AC\uC138\uC694.")}
|
|
3373
|
+
${colorField("litColor", "effect.litColor", eff.litColor)}
|
|
3374
|
+
<button type="button" class="studio-btn studio-btn-danger" data-delete-effect style="margin-top:0.6rem">\u{1F5D1} \uD6A8\uACFC \uC0AD\uC81C</button>
|
|
3375
|
+
`;
|
|
3376
|
+
return;
|
|
3377
|
+
}
|
|
3069
3378
|
const elemOpts = def.elements.map(
|
|
3070
3379
|
(e) => `<option value="${escapeHtml2(e.id)}" ${e.id === eff.elementId ? "selected" : ""}>${escapeHtml2(e.id)}</option>`
|
|
3071
3380
|
).join("");
|
|
3072
|
-
const typeOpts = ["highlight", "pulse", "flow"].map(
|
|
3073
|
-
(t) => `<option value="${t}" ${t === eff.type ? "selected" : ""}>${t}</option>`
|
|
3074
|
-
).join("");
|
|
3075
3381
|
const specific = eff.type === "highlight" ? colorField("color", "effect.color", eff.color) : eff.type === "pulse" ? numberField("scale", "effect.scale", eff.scale, 0.05) : colorField("color", "effect.color", eff.color) + numberField("particles", "effect.particles", eff.particles, 1) + numberField("radius", "effect.radius", eff.radius, 0.5);
|
|
3076
3382
|
panelEl.innerHTML = `
|
|
3077
3383
|
${timeHint}
|
|
@@ -3086,6 +3392,114 @@ function renderInner() {
|
|
|
3086
3392
|
return;
|
|
3087
3393
|
}
|
|
3088
3394
|
}
|
|
3395
|
+
function rangeField(label, key, value, min, max, step, hint) {
|
|
3396
|
+
return `<div class="studio-camera-range">
|
|
3397
|
+
<div class="studio-camera-range-head"><span>${escapeHtml2(label)}</span><span class="studio-camera-range-value">${value}</span></div>
|
|
3398
|
+
<div class="studio-camera-range-row">
|
|
3399
|
+
<input type="range" data-prop-key="${escapeHtml2(key)}" min="${min}" max="${max}" step="${step}" value="${value}" />
|
|
3400
|
+
<input type="number" data-prop-key="${escapeHtml2(key)}" step="${step}" value="${value}" />
|
|
3401
|
+
</div>
|
|
3402
|
+
${hint ? `<p class="studio-camera-hint">${escapeHtml2(hint)}</p>` : ""}
|
|
3403
|
+
</div>`;
|
|
3404
|
+
}
|
|
3405
|
+
function elementCheckList(def, attribute, key, selected) {
|
|
3406
|
+
const chosen = new Set(selected);
|
|
3407
|
+
const rows = def.elements.map((element) => {
|
|
3408
|
+
const name = friendlyElementLabel(element);
|
|
3409
|
+
const same = name === element.id;
|
|
3410
|
+
return `<label class="studio-camera-target ${chosen.has(element.id) ? "is-on" : ""}">
|
|
3411
|
+
<input type="checkbox" ${attribute}="${escapeHtml2(key)}" value="${escapeHtml2(element.id)}" ${chosen.has(element.id) ? "checked" : ""} />
|
|
3412
|
+
<span class="studio-camera-target-name">${escapeHtml2(name)}</span>
|
|
3413
|
+
${same ? "" : `<span class="studio-camera-target-id">${escapeHtml2(element.id)}</span>`}
|
|
3414
|
+
<span class="studio-camera-target-type">${element.type}</span>
|
|
3415
|
+
</label>`;
|
|
3416
|
+
}).join("");
|
|
3417
|
+
const missing = selected.filter(
|
|
3418
|
+
(id) => !def.elements.some((element) => element.id === id)
|
|
3419
|
+
);
|
|
3420
|
+
return `<div class="studio-camera-targets">
|
|
3421
|
+
${rows || '<p class="studio-props-empty">\uC694\uC18C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</p>'}
|
|
3422
|
+
${missing.length > 0 ? `<p class="studio-camera-issue">\u26A0 \uBB38\uC11C\uC5D0 \uC5C6\uB294 id: ${escapeHtml2(missing.join(", "))}</p>` : ""}
|
|
3423
|
+
</div>`;
|
|
3424
|
+
}
|
|
3425
|
+
function renderCameraForm(def, focusIndex) {
|
|
3426
|
+
if (!panelEl) return;
|
|
3427
|
+
const camera = getCamera();
|
|
3428
|
+
const time = getCurrentTime();
|
|
3429
|
+
const view = computeCamera(def, time);
|
|
3430
|
+
const control = cameraControlAt(time);
|
|
3431
|
+
const scalingOpts = ["scale", "fixed"].map(
|
|
3432
|
+
(value) => `<option value="${value}" ${value === camera.strokeScaling ? "selected" : ""}>${value}${value === "scale" ? " (\uAE30\uBCF8)" : ""}</option>`
|
|
3433
|
+
).join("");
|
|
3434
|
+
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";
|
|
3435
|
+
const readout = view ? `<div class="studio-camera-readout">zoom ${view.zoom.toFixed(2)}\xD7 \xB7 center ${Math.round(view.centerX)}, ${Math.round(view.centerY)}
|
|
3436
|
+
<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>';
|
|
3437
|
+
const focusRows = camera.focus.map((entry, index) => {
|
|
3438
|
+
const open = index === focusIndex;
|
|
3439
|
+
const targets = entry.elementIds.join(", ");
|
|
3440
|
+
const preset = (value, label) => `<button type="button" class="studio-chip ${entry.duration === value ? "is-on" : ""}" data-camera-duration="${index}:${value}">${label}</button>`;
|
|
3441
|
+
return `
|
|
3442
|
+
<details class="studio-camera-focus" ${open ? "open" : ""}>
|
|
3443
|
+
<summary>#${index + 1} \xB7 ${entry.time}ms \u2192 ${escapeHtml2(targets)}</summary>
|
|
3444
|
+
<div class="studio-camera-focus-actions">
|
|
3445
|
+
<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>
|
|
3446
|
+
<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>
|
|
3447
|
+
</div>
|
|
3448
|
+
${numberField("time (ms)", `camera.focus.${index}.time`, entry.time, 50)}
|
|
3449
|
+
<div class="studio-camera-range">
|
|
3450
|
+
<div class="studio-camera-range-head"><span>duration (ms)</span><span class="studio-camera-range-value">${entry.duration}</span></div>
|
|
3451
|
+
<div class="studio-camera-chips">${preset(0, "\uCEF7")}${preset(300, "300")}${preset(600, "600")}${preset(1e3, "1000")}${preset(1600, "1600")}</div>
|
|
3452
|
+
<div class="studio-camera-range-row">
|
|
3453
|
+
<input type="range" data-prop-key="camera.focus.${index}.duration" min="0" max="3000" step="50" value="${entry.duration}" />
|
|
3454
|
+
<input type="number" data-prop-key="camera.focus.${index}.duration" step="50" value="${entry.duration}" />
|
|
3455
|
+
</div>
|
|
3456
|
+
</div>
|
|
3457
|
+
<div class="studio-camera-field-label">\uB300\uC0C1 \uC694\uC18C (${entry.elementIds.length})</div>
|
|
3458
|
+
${elementCheckList(def, "data-camera-target", String(index), entry.elementIds)}
|
|
3459
|
+
${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.")}
|
|
3460
|
+
${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.")}
|
|
3461
|
+
<button type="button" class="studio-btn studio-btn-danger" data-delete-camera-focus="${index}">\u{1F5D1} focus \uC0AD\uC81C</button>
|
|
3462
|
+
</details>`;
|
|
3463
|
+
}).join("");
|
|
3464
|
+
const trackRows = ["zoom", "x", "y"].map((property) => {
|
|
3465
|
+
const track = camera.tracks.find((t) => t.property === property);
|
|
3466
|
+
const current = property === "zoom" ? view?.zoom ?? 1 : property === "x" ? view?.centerX ?? def.canvas.width / 2 : view?.centerY ?? def.canvas.height / 2;
|
|
3467
|
+
const keyframes = (track?.keyframes ?? []).map(
|
|
3468
|
+
(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>`
|
|
3469
|
+
).join("");
|
|
3470
|
+
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 };
|
|
3471
|
+
return `
|
|
3472
|
+
<div class="studio-camera-track">
|
|
3473
|
+
<div class="studio-camera-track-head">
|
|
3474
|
+
<span>${property}</span>
|
|
3475
|
+
<span class="studio-camera-track-now">\uD604\uC7AC ${current.toFixed(property === "zoom" ? 2 : 0)}</span>
|
|
3476
|
+
${track ? `<button type="button" class="studio-btn studio-btn-danger" data-del-camera-track="${property}" title="track \uC0AD\uC81C">\u{1F5D1}</button>` : ""}
|
|
3477
|
+
</div>
|
|
3478
|
+
${control.kind === "focus" ? `<div class="studio-camera-range-row">
|
|
3479
|
+
<input type="number" data-prop-key="camera.kf.${property}" step="${bounds.step}" value="${current.toFixed(property === "zoom" ? 2 : 0)}" />
|
|
3480
|
+
</div>
|
|
3481
|
+
<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">
|
|
3482
|
+
<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)}" />
|
|
3483
|
+
<input type="number" data-prop-key="camera.kf.${property}" step="${bounds.step}" value="${current.toFixed(property === "zoom" ? 2 : 0)}" />
|
|
3484
|
+
</div>
|
|
3485
|
+
<p class="studio-camera-hint">\uC6C0\uC9C1\uC774\uBA74 t = ${time}ms \uC5D0 keyframe\uC744 \uC501\uB2C8\uB2E4.</p>`}
|
|
3486
|
+
${keyframes ? `<ul class="studio-camera-kf-list">${keyframes}</ul>` : '<p class="studio-props-empty">keyframe \uC5C6\uC74C</p>'}
|
|
3487
|
+
</div>`;
|
|
3488
|
+
}).join("");
|
|
3489
|
+
panelEl.innerHTML = `
|
|
3490
|
+
<span class="studio-step-hint">\u{1F4CD} t = ${time} ms / ${def.duration} ms</span>
|
|
3491
|
+
<div class="studio-props-header"><span class="studio-props-header-title">\uCE74\uBA54\uB77C</span><span class="studio-props-header-type">camera</span></div>
|
|
3492
|
+
${readout}
|
|
3493
|
+
<label class="studio-field"><span>strokeScaling</span><select data-prop-key="camera.strokeScaling">${scalingOpts}</select></label>
|
|
3494
|
+
<div class="studio-props-header" style="margin-top:0.6rem"><span class="studio-props-header-title">focus (${camera.focus.length})</span></div>
|
|
3495
|
+
<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>
|
|
3496
|
+
${focusRows}
|
|
3497
|
+
<button type="button" class="studio-btn" data-add-camera-focus style="margin-top:0.4rem">\uFF0B \uD604\uC7AC \uC2DC\uAC01\uC5D0 focus</button>
|
|
3498
|
+
<div class="studio-props-header" style="margin-top:0.6rem"><span class="studio-props-header-title">tracks</span></div>
|
|
3499
|
+
${trackRows}
|
|
3500
|
+
${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>' : ""}
|
|
3501
|
+
`;
|
|
3502
|
+
}
|
|
3089
3503
|
function renderElementForm(def, el) {
|
|
3090
3504
|
if (!panelEl) return;
|
|
3091
3505
|
const timeHint = `<span class="studio-step-hint">\u{1F4CD} t = ${getCurrentTime()} ms</span>`;
|
|
@@ -3367,6 +3781,16 @@ function renderTracks(el) {
|
|
|
3367
3781
|
}
|
|
3368
3782
|
function onInput(e) {
|
|
3369
3783
|
const target = e.target;
|
|
3784
|
+
const focusTarget = target.dataset.cameraTarget;
|
|
3785
|
+
if (focusTarget !== void 0 && target instanceof HTMLInputElement) {
|
|
3786
|
+
toggleFocusTarget(Number(focusTarget), target.value, target.checked);
|
|
3787
|
+
return;
|
|
3788
|
+
}
|
|
3789
|
+
const spotTarget = target.dataset.spotlightTarget;
|
|
3790
|
+
if (spotTarget !== void 0 && target instanceof HTMLInputElement) {
|
|
3791
|
+
toggleSpotlightTarget(spotTarget, target.value, target.checked);
|
|
3792
|
+
return;
|
|
3793
|
+
}
|
|
3370
3794
|
const key = target.dataset.propKey;
|
|
3371
3795
|
if (!key) return;
|
|
3372
3796
|
if (target.type === "text" && e.isComposing) return;
|
|
@@ -3385,6 +3809,16 @@ function onInput(e) {
|
|
|
3385
3809
|
value = target.checked;
|
|
3386
3810
|
apply(key, value);
|
|
3387
3811
|
}
|
|
3812
|
+
function toggleFocusTarget(index, id, on) {
|
|
3813
|
+
const entry = getCamera().focus[index];
|
|
3814
|
+
if (!entry) return;
|
|
3815
|
+
const next = on ? [...entry.elementIds, id] : entry.elementIds.filter((value) => value !== id);
|
|
3816
|
+
if (next.length === 0) {
|
|
3817
|
+
render3();
|
|
3818
|
+
return;
|
|
3819
|
+
}
|
|
3820
|
+
updateCameraFocus(index, { elementIds: next });
|
|
3821
|
+
}
|
|
3388
3822
|
function onChange(e) {
|
|
3389
3823
|
const target = e.target;
|
|
3390
3824
|
if (target instanceof HTMLInputElement && target.type === "color") {
|
|
@@ -3485,6 +3919,33 @@ function apply(key, value) {
|
|
|
3485
3919
|
predicate: { type: "equals", value: answer }
|
|
3486
3920
|
});
|
|
3487
3921
|
}
|
|
3922
|
+
} else if (key === "camera.strokeScaling")
|
|
3923
|
+
setCameraStrokeScaling(String(value));
|
|
3924
|
+
else if (key.startsWith("camera.kf.")) {
|
|
3925
|
+
const property = key.slice("camera.kf.".length);
|
|
3926
|
+
const raw = Number(value);
|
|
3927
|
+
if (!Number.isFinite(raw)) return;
|
|
3928
|
+
setCameraKeyframe(
|
|
3929
|
+
property,
|
|
3930
|
+
getCurrentTime(),
|
|
3931
|
+
property === "zoom" ? Math.max(0.05, raw) : Math.round(raw)
|
|
3932
|
+
);
|
|
3933
|
+
} else if (key.startsWith("camera.focus.")) {
|
|
3934
|
+
const [, , indexText, field] = key.split(".");
|
|
3935
|
+
const index = Number(indexText);
|
|
3936
|
+
if (!Number.isInteger(index)) return;
|
|
3937
|
+
if (field === "elementIds") {
|
|
3938
|
+
const ids = String(value).split(",").map((id) => id.trim()).filter(Boolean);
|
|
3939
|
+
if (ids.length > 0) updateCameraFocus(index, { elementIds: ids });
|
|
3940
|
+
return;
|
|
3941
|
+
}
|
|
3942
|
+
if (field === "time") updateCameraFocus(index, { time: Number(value) });
|
|
3943
|
+
else if (field === "duration")
|
|
3944
|
+
updateCameraFocus(index, { duration: Math.max(0, Number(value)) });
|
|
3945
|
+
else if (field === "padding")
|
|
3946
|
+
updateCameraFocus(index, { padding: Math.max(0, Number(value)) });
|
|
3947
|
+
else if (field === "maxZoom")
|
|
3948
|
+
updateCameraFocus(index, { maxZoom: Math.max(0.01, Number(value)) });
|
|
3488
3949
|
} else if (key.startsWith("el.") && sel.kind === "element") {
|
|
3489
3950
|
const prop = key.slice(3);
|
|
3490
3951
|
const time = getCurrentTime();
|
|
@@ -3579,10 +4040,12 @@ function apply(key, value) {
|
|
|
3579
4040
|
if (targets) references[token] = targets;
|
|
3580
4041
|
else delete references[token];
|
|
3581
4042
|
updateChapter(sel.chapterId, { references });
|
|
4043
|
+
} else if (key === "effect.type" && sel.kind === "effect") {
|
|
4044
|
+
changeEffectType(def, sel.effectId, String(value));
|
|
3582
4045
|
} else if (key.startsWith("effect.") && sel.kind === "effect") {
|
|
3583
4046
|
const prop = key.slice(7);
|
|
3584
4047
|
const patch = {};
|
|
3585
|
-
if (prop === "time" || prop === "duration" || prop === "scale" || prop === "particles" || prop === "radius") {
|
|
4048
|
+
if (prop === "time" || prop === "duration" || prop === "scale" || prop === "particles" || prop === "radius" || prop === "dim" || prop === "lit" || prop === "padding" || prop === "fadeIn") {
|
|
3586
4049
|
patch[prop] = Number(value);
|
|
3587
4050
|
} else {
|
|
3588
4051
|
patch[prop] = value;
|
|
@@ -3590,6 +4053,52 @@ function apply(key, value) {
|
|
|
3590
4053
|
updateEffect(sel.effectId, patch);
|
|
3591
4054
|
}
|
|
3592
4055
|
}
|
|
4056
|
+
function changeEffectType(def, effectId, type) {
|
|
4057
|
+
const current = def.effects.find((effect) => effect.id === effectId);
|
|
4058
|
+
if (!current || current.type === type) return;
|
|
4059
|
+
const targets = current.type === "spotlight" ? current.elementIds : [current.elementId];
|
|
4060
|
+
const base = { id: current.id, time: current.time };
|
|
4061
|
+
let next;
|
|
4062
|
+
if (type === "spotlight") {
|
|
4063
|
+
next = {
|
|
4064
|
+
...base,
|
|
4065
|
+
type: "spotlight",
|
|
4066
|
+
elementIds: targets,
|
|
4067
|
+
duration: Math.max(current.duration, 800),
|
|
4068
|
+
dim: 0.7,
|
|
4069
|
+
lit: 0,
|
|
4070
|
+
litColor: "#fde68a",
|
|
4071
|
+
shape: "bbox",
|
|
4072
|
+
padding: 12,
|
|
4073
|
+
fadeIn: 200
|
|
4074
|
+
};
|
|
4075
|
+
} else {
|
|
4076
|
+
const single = {
|
|
4077
|
+
...base,
|
|
4078
|
+
elementId: targets[0] ?? "",
|
|
4079
|
+
duration: current.duration
|
|
4080
|
+
};
|
|
4081
|
+
next = type === "pulse" ? { ...single, type: "pulse", scale: 1.12 } : type === "flow" ? {
|
|
4082
|
+
...single,
|
|
4083
|
+
type: "flow",
|
|
4084
|
+
color: "#facc15",
|
|
4085
|
+
particles: 3,
|
|
4086
|
+
radius: 4
|
|
4087
|
+
} : { ...single, type: "highlight", color: "#facc15" };
|
|
4088
|
+
}
|
|
4089
|
+
replaceEffect(effectId, next);
|
|
4090
|
+
}
|
|
4091
|
+
function toggleSpotlightTarget(effectId, id, on) {
|
|
4092
|
+
const def = getDef();
|
|
4093
|
+
const effect = def?.effects.find((candidate) => candidate.id === effectId);
|
|
4094
|
+
if (!effect || effect.type !== "spotlight") return;
|
|
4095
|
+
const next = on ? [...effect.elementIds, id] : effect.elementIds.filter((target) => target !== id);
|
|
4096
|
+
if (next.length === 0) {
|
|
4097
|
+
render3();
|
|
4098
|
+
return;
|
|
4099
|
+
}
|
|
4100
|
+
updateEffect(effectId, { elementIds: next });
|
|
4101
|
+
}
|
|
3593
4102
|
function parseBindingFallback(value) {
|
|
3594
4103
|
if (value === "") return void 0;
|
|
3595
4104
|
if (value === "true") return true;
|
|
@@ -3682,6 +4191,74 @@ function onClick2(e) {
|
|
|
3682
4191
|
});
|
|
3683
4192
|
return;
|
|
3684
4193
|
}
|
|
4194
|
+
if (target.closest("[data-open-camera]")) {
|
|
4195
|
+
setSelection({ kind: "camera" });
|
|
4196
|
+
return;
|
|
4197
|
+
}
|
|
4198
|
+
if (target.closest("[data-add-camera-focus]")) {
|
|
4199
|
+
const seed = sel.kind === "element" ? [sel.elementId] : sel.kind === "elements" ? [...sel.elementIds] : def.elements[0] ? [def.elements[0].id] : [];
|
|
4200
|
+
if (seed.length === 0) return;
|
|
4201
|
+
addCameraFocus({
|
|
4202
|
+
time: getCurrentTime(),
|
|
4203
|
+
duration: 600,
|
|
4204
|
+
elementIds: seed,
|
|
4205
|
+
padding: 24,
|
|
4206
|
+
maxZoom: 4
|
|
4207
|
+
});
|
|
4208
|
+
return;
|
|
4209
|
+
}
|
|
4210
|
+
const durationChip = target.closest("[data-camera-duration]");
|
|
4211
|
+
if (durationChip) {
|
|
4212
|
+
const [index, duration] = (durationChip.dataset.cameraDuration ?? "").split(
|
|
4213
|
+
":"
|
|
4214
|
+
);
|
|
4215
|
+
updateCameraFocus(Number(index), { duration: Number(duration) });
|
|
4216
|
+
return;
|
|
4217
|
+
}
|
|
4218
|
+
const focusNow = target.closest("[data-camera-focus-now]");
|
|
4219
|
+
if (focusNow) {
|
|
4220
|
+
updateCameraFocus(Number(focusNow.dataset.cameraFocusNow), {
|
|
4221
|
+
time: getCurrentTime()
|
|
4222
|
+
});
|
|
4223
|
+
return;
|
|
4224
|
+
}
|
|
4225
|
+
const deleteFocus = target.closest("[data-delete-camera-focus]");
|
|
4226
|
+
if (deleteFocus) {
|
|
4227
|
+
deleteCameraFocus(Number(deleteFocus.dataset.deleteCameraFocus));
|
|
4228
|
+
return;
|
|
4229
|
+
}
|
|
4230
|
+
const addCameraKf = target.closest("[data-add-camera-kf]");
|
|
4231
|
+
if (addCameraKf) {
|
|
4232
|
+
const property = addCameraKf.dataset.addCameraKf;
|
|
4233
|
+
const view = computeCamera(def, getCurrentTime());
|
|
4234
|
+
const value = property === "zoom" ? view?.zoom ?? 1 : property === "x" ? view?.centerX ?? def.canvas.width / 2 : view?.centerY ?? def.canvas.height / 2;
|
|
4235
|
+
setCameraKeyframe(
|
|
4236
|
+
property,
|
|
4237
|
+
getCurrentTime(),
|
|
4238
|
+
Number(value.toFixed(property === "zoom" ? 3 : 1))
|
|
4239
|
+
);
|
|
4240
|
+
return;
|
|
4241
|
+
}
|
|
4242
|
+
const delCameraKf = target.closest("[data-camera-kf-del]");
|
|
4243
|
+
if (delCameraKf) {
|
|
4244
|
+
const [property, at] = (delCameraKf.dataset.cameraKfDel ?? "").split(":");
|
|
4245
|
+
removeCameraKeyframe(property, Number(at));
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
4248
|
+
const delCameraTrack = target.closest("[data-del-camera-track]");
|
|
4249
|
+
if (delCameraTrack) {
|
|
4250
|
+
removeCameraTrack(delCameraTrack.dataset.delCameraTrack);
|
|
4251
|
+
return;
|
|
4252
|
+
}
|
|
4253
|
+
const cameraSeek = target.closest("[data-camera-seek]");
|
|
4254
|
+
if (cameraSeek) {
|
|
4255
|
+
setCurrentTime(Number(cameraSeek.dataset.cameraSeek));
|
|
4256
|
+
return;
|
|
4257
|
+
}
|
|
4258
|
+
if (target.closest("[data-clear-camera]")) {
|
|
4259
|
+
clearCamera();
|
|
4260
|
+
return;
|
|
4261
|
+
}
|
|
3685
4262
|
if (target.closest("[data-add-chapter]")) {
|
|
3686
4263
|
const id = uniqueChapterId();
|
|
3687
4264
|
addChapter({
|
|
@@ -3712,7 +4289,22 @@ function onClick2(e) {
|
|
|
3712
4289
|
duration: 500
|
|
3713
4290
|
};
|
|
3714
4291
|
let eff;
|
|
3715
|
-
if (type === "
|
|
4292
|
+
if (type === "spotlight") {
|
|
4293
|
+
const selected = sel.kind === "element" ? [sel.elementId] : sel.kind === "elements" ? [...sel.elementIds] : [firstEl.id];
|
|
4294
|
+
eff = {
|
|
4295
|
+
id,
|
|
4296
|
+
type: "spotlight",
|
|
4297
|
+
elementIds: selected,
|
|
4298
|
+
time: getCurrentTime(),
|
|
4299
|
+
duration: 1200,
|
|
4300
|
+
dim: 0.7,
|
|
4301
|
+
lit: 0,
|
|
4302
|
+
litColor: "#fde68a",
|
|
4303
|
+
shape: "bbox",
|
|
4304
|
+
padding: 12,
|
|
4305
|
+
fadeIn: 200
|
|
4306
|
+
};
|
|
4307
|
+
} else if (type === "highlight")
|
|
3716
4308
|
eff = { ...base, type: "highlight", color: "#facc15" };
|
|
3717
4309
|
else if (type === "pulse") eff = { ...base, type: "pulse", scale: 1.12 };
|
|
3718
4310
|
else
|
|
@@ -3930,6 +4522,7 @@ function render4() {
|
|
|
3930
4522
|
<div class="studio-tl-chapter-label">${escapeHtml3(c.label || c.id)}</div>
|
|
3931
4523
|
</div>`;
|
|
3932
4524
|
}).join("");
|
|
4525
|
+
const cameraRows = renderCameraRows(totalPx, sel);
|
|
3933
4526
|
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
4527
|
tracksEl.innerHTML = `
|
|
3935
4528
|
<div class="studio-tl-chart">
|
|
@@ -3947,12 +4540,48 @@ function render4() {
|
|
|
3947
4540
|
</div>
|
|
3948
4541
|
</div>
|
|
3949
4542
|
</div>
|
|
4543
|
+
${cameraRows}
|
|
3950
4544
|
${playheadCol}
|
|
3951
4545
|
</div>
|
|
3952
4546
|
<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
4547
|
`;
|
|
3954
4548
|
renderElementTracks(def.elements, currentTime, totalPx, sel);
|
|
3955
4549
|
}
|
|
4550
|
+
function renderCameraRows(totalPx, sel) {
|
|
4551
|
+
const camera = getCamera();
|
|
4552
|
+
const isCameraSel = sel.kind === "camera";
|
|
4553
|
+
const focusBars = camera.focus.map((entry, index) => {
|
|
4554
|
+
const left = timeToPx(entry.time);
|
|
4555
|
+
const width = Math.max(6, timeToPx(entry.duration));
|
|
4556
|
+
const selected = isCameraSel && sel.focusIndex === index;
|
|
4557
|
+
const targets = entry.elementIds.join(", ");
|
|
4558
|
+
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)">
|
|
4559
|
+
<span class="studio-tl-camera-focus-label">${escapeHtml3(targets)}</span>
|
|
4560
|
+
</div>`;
|
|
4561
|
+
}).join("");
|
|
4562
|
+
const focusRow = `
|
|
4563
|
+
<div class="studio-tl-row studio-tl-camera-row ${isCameraSel && sel.focusIndex === void 0 ? "is-selected" : ""}">
|
|
4564
|
+
<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>
|
|
4565
|
+
<div class="studio-tl-body" style="width:${totalPx}px">
|
|
4566
|
+
<div class="studio-tl-camera-track" data-tl-area="camera">
|
|
4567
|
+
${focusBars || '<span class="studio-tl-camera-empty">focus \uC5C6\uC74C \xB7 \uFF0B \uB85C \uCD94\uAC00</span>'}
|
|
4568
|
+
</div>
|
|
4569
|
+
</div>
|
|
4570
|
+
</div>`;
|
|
4571
|
+
const trackRows = camera.tracks.map((track) => {
|
|
4572
|
+
const marks = track.keyframes.map(
|
|
4573
|
+
(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>`
|
|
4574
|
+
).join("");
|
|
4575
|
+
return `
|
|
4576
|
+
<div class="studio-tl-row studio-tl-camera-row">
|
|
4577
|
+
<div class="studio-tl-gutter studio-tl-camera-gutter is-sub" data-camera-select>\u21B3 ${track.property}</div>
|
|
4578
|
+
<div class="studio-tl-body" style="width:${totalPx}px">
|
|
4579
|
+
<div class="studio-tl-camera-track" data-tl-area="camera">${marks}</div>
|
|
4580
|
+
</div>
|
|
4581
|
+
</div>`;
|
|
4582
|
+
}).join("");
|
|
4583
|
+
return focusRow + trackRows;
|
|
4584
|
+
}
|
|
3956
4585
|
function gutterLabel(el) {
|
|
3957
4586
|
return `${friendlyElementLabel(el)} \xB7 ${el.type}`;
|
|
3958
4587
|
}
|
|
@@ -4008,6 +4637,32 @@ function renderElementTracks(elements, currentTime, totalPx, sel) {
|
|
|
4008
4637
|
}
|
|
4009
4638
|
function onTracksClick(e) {
|
|
4010
4639
|
const target = e.target;
|
|
4640
|
+
if (target.closest("[data-add-camera-focus]")) {
|
|
4641
|
+
const time = getCurrentTime();
|
|
4642
|
+
const selected = getSelection();
|
|
4643
|
+
const elementIds = selected.kind === "element" ? [selected.elementId] : selected.kind === "elements" ? [...selected.elementIds] : (getDef()?.elements[0]?.id ?? "").length > 0 ? [getDef().elements[0].id] : [];
|
|
4644
|
+
if (elementIds.length === 0) return;
|
|
4645
|
+
addCameraFocus({
|
|
4646
|
+
time,
|
|
4647
|
+
duration: 600,
|
|
4648
|
+
elementIds,
|
|
4649
|
+
padding: 24,
|
|
4650
|
+
maxZoom: 4
|
|
4651
|
+
});
|
|
4652
|
+
return;
|
|
4653
|
+
}
|
|
4654
|
+
const focusBar = target.closest("[data-camera-focus]");
|
|
4655
|
+
if (focusBar) {
|
|
4656
|
+
setSelection({
|
|
4657
|
+
kind: "camera",
|
|
4658
|
+
focusIndex: Number(focusBar.dataset.cameraFocus)
|
|
4659
|
+
});
|
|
4660
|
+
return;
|
|
4661
|
+
}
|
|
4662
|
+
if (target.closest("[data-camera-select]")) {
|
|
4663
|
+
setSelection({ kind: "camera" });
|
|
4664
|
+
return;
|
|
4665
|
+
}
|
|
4011
4666
|
const chapterMarker = target.closest("[data-chapter-id]");
|
|
4012
4667
|
if (chapterMarker) {
|
|
4013
4668
|
setSelection({
|
|
@@ -4097,6 +4752,36 @@ function onMouseDown2(e) {
|
|
|
4097
4752
|
}
|
|
4098
4753
|
return;
|
|
4099
4754
|
}
|
|
4755
|
+
const cameraKf = target.closest("[data-camera-kf-prop]");
|
|
4756
|
+
if (cameraKf) {
|
|
4757
|
+
e.preventDefault();
|
|
4758
|
+
e.stopPropagation();
|
|
4759
|
+
const startTime = Number(cameraKf.dataset.cameraKfTime);
|
|
4760
|
+
dragMode = {
|
|
4761
|
+
kind: "camera-keyframe",
|
|
4762
|
+
prop: cameraKf.dataset.cameraKfProp,
|
|
4763
|
+
startTime,
|
|
4764
|
+
startMouseX: e.clientX,
|
|
4765
|
+
currentTime: startTime
|
|
4766
|
+
};
|
|
4767
|
+
return;
|
|
4768
|
+
}
|
|
4769
|
+
const cameraFocus = target.closest("[data-camera-focus]");
|
|
4770
|
+
if (cameraFocus) {
|
|
4771
|
+
e.preventDefault();
|
|
4772
|
+
e.stopPropagation();
|
|
4773
|
+
const index = Number(cameraFocus.dataset.cameraFocus);
|
|
4774
|
+
const entry = getCamera().focus[index];
|
|
4775
|
+
if (entry) {
|
|
4776
|
+
dragMode = {
|
|
4777
|
+
kind: "camera-focus",
|
|
4778
|
+
index,
|
|
4779
|
+
startTime: entry.time,
|
|
4780
|
+
startMouseX: e.clientX
|
|
4781
|
+
};
|
|
4782
|
+
}
|
|
4783
|
+
return;
|
|
4784
|
+
}
|
|
4100
4785
|
const chapterMarker = target.closest("[data-chapter-id]");
|
|
4101
4786
|
if (chapterMarker) {
|
|
4102
4787
|
e.preventDefault();
|
|
@@ -4157,6 +4842,29 @@ function onMouseMove2(e) {
|
|
|
4157
4842
|
showDragTooltip(e.clientX, e.clientY, `\u25C6 ${dragMode.prop} @ ${newTime} ms`);
|
|
4158
4843
|
return;
|
|
4159
4844
|
}
|
|
4845
|
+
if (dragMode.kind === "camera-keyframe") {
|
|
4846
|
+
const dt = Math.round((e.clientX - dragMode.startMouseX) / pxPerMs);
|
|
4847
|
+
const newTime = Math.max(0, dragMode.startTime + dt);
|
|
4848
|
+
if (newTime !== dragMode.currentTime) {
|
|
4849
|
+
moveCameraKeyframe(dragMode.prop, dragMode.currentTime, newTime);
|
|
4850
|
+
dragMode.currentTime = newTime;
|
|
4851
|
+
}
|
|
4852
|
+
showDragTooltip(
|
|
4853
|
+
e.clientX,
|
|
4854
|
+
e.clientY,
|
|
4855
|
+
`\u{1F3A5} ${dragMode.prop} @ ${newTime} ms`
|
|
4856
|
+
);
|
|
4857
|
+
return;
|
|
4858
|
+
}
|
|
4859
|
+
if (dragMode.kind === "camera-focus") {
|
|
4860
|
+
const dt = Math.round((e.clientX - dragMode.startMouseX) / pxPerMs);
|
|
4861
|
+
const newTime = Math.max(0, dragMode.startTime + dt);
|
|
4862
|
+
updateCameraFocus(dragMode.index, { time: newTime });
|
|
4863
|
+
const moved = getCamera().focus.findIndex((f) => f.time === newTime);
|
|
4864
|
+
if (moved >= 0) dragMode.index = moved;
|
|
4865
|
+
showDragTooltip(e.clientX, e.clientY, `\u{1F3A5} focus @ ${newTime} ms`);
|
|
4866
|
+
return;
|
|
4867
|
+
}
|
|
4160
4868
|
if (dragMode.kind === "appearance") {
|
|
4161
4869
|
const dxPx = e.clientX - dragMode.startMouseX;
|
|
4162
4870
|
const dt = Math.round(dxPx / pxPerMs);
|
|
@@ -16159,6 +16867,7 @@ var KIND_ICON = {
|
|
|
16159
16867
|
appearance: "\u25D0",
|
|
16160
16868
|
chapter: "\u{1F4CD}",
|
|
16161
16869
|
effect: "\u2728",
|
|
16870
|
+
camera: "\u{1F3A5}",
|
|
16162
16871
|
asset: "\u{1F5BC}",
|
|
16163
16872
|
group: "\u2B1A",
|
|
16164
16873
|
layout: "\u25A6",
|
|
@@ -16283,7 +16992,8 @@ function queryUi() {
|
|
|
16283
16992
|
const undoBtn = $("studio-undo");
|
|
16284
16993
|
const redoBtn = $("studio-redo");
|
|
16285
16994
|
const gridToggleBtn = $("studio-grid-toggle");
|
|
16286
|
-
|
|
16995
|
+
const cameraFrameBtn = $("studio-camera-frame-toggle");
|
|
16996
|
+
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
16997
|
return null;
|
|
16288
16998
|
}
|
|
16289
16999
|
return {
|
|
@@ -16326,7 +17036,8 @@ function queryUi() {
|
|
|
16326
17036
|
helpDialog,
|
|
16327
17037
|
undoBtn,
|
|
16328
17038
|
redoBtn,
|
|
16329
|
-
gridToggleBtn
|
|
17039
|
+
gridToggleBtn,
|
|
17040
|
+
cameraFrameBtn
|
|
16330
17041
|
};
|
|
16331
17042
|
}
|
|
16332
17043
|
|
|
@@ -16434,6 +17145,14 @@ function reflectGridUi(ui) {
|
|
|
16434
17145
|
const label = document.getElementById("studio-grid-label");
|
|
16435
17146
|
if (label) label.textContent = on ? `\uACA9\uC790 ${size}px` : "\uACA9\uC790 \uB054";
|
|
16436
17147
|
}
|
|
17148
|
+
function reflectCameraFrameUi(ui) {
|
|
17149
|
+
const on = isCameraFrameVisible();
|
|
17150
|
+
ui.cameraFrameBtn.setAttribute("aria-pressed", on ? "true" : "false");
|
|
17151
|
+
ui.cameraFrameBtn.classList.toggle("is-active", on);
|
|
17152
|
+
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";
|
|
17153
|
+
const label = document.getElementById("studio-camera-frame-label");
|
|
17154
|
+
if (label) label.textContent = on ? "\uCE74\uBA54\uB77C \uC601\uC5ED" : "\uCE74\uBA54\uB77C \uC601\uC5ED \uB054";
|
|
17155
|
+
}
|
|
16437
17156
|
function setupTimelineResizer(ui) {
|
|
16438
17157
|
const resizer = document.getElementById("studio-timeline-resizer");
|
|
16439
17158
|
if (!resizer) return;
|
|
@@ -17109,6 +17828,7 @@ function initStudio(opts = {}) {
|
|
|
17109
17828
|
initGrid();
|
|
17110
17829
|
reflectGridUi(ui);
|
|
17111
17830
|
subscribeGrid(() => reflectGridUi(ui));
|
|
17831
|
+
reflectCameraFrameUi(ui);
|
|
17112
17832
|
initCanvas(ui.canvas);
|
|
17113
17833
|
initElementList(ui.elementList, ui.toolsRoot);
|
|
17114
17834
|
initProperties(ui.propsRoot);
|
|
@@ -17282,6 +18002,10 @@ function initStudio(opts = {}) {
|
|
|
17282
18002
|
"click",
|
|
17283
18003
|
() => setGridEnabled(!isGridEnabled())
|
|
17284
18004
|
);
|
|
18005
|
+
ui.cameraFrameBtn.addEventListener("click", () => {
|
|
18006
|
+
setCameraFrameVisible(!isCameraFrameVisible());
|
|
18007
|
+
reflectCameraFrameUi(ui);
|
|
18008
|
+
});
|
|
17285
18009
|
ui.playBtn.addEventListener("click", () => togglePlay(ui));
|
|
17286
18010
|
ui.restartBtn.addEventListener("click", () => {
|
|
17287
18011
|
if (isPlaying()) {
|
|
@@ -17372,5 +18096,5 @@ function reflectState(ui) {
|
|
|
17372
18096
|
}
|
|
17373
18097
|
|
|
17374
18098
|
export { getVisibleElementIds, initStudio };
|
|
17375
|
-
//# sourceMappingURL=main-
|
|
17376
|
-
//# sourceMappingURL=main-
|
|
18099
|
+
//# sourceMappingURL=main-HDE5SDKM.js.map
|
|
18100
|
+
//# sourceMappingURL=main-HDE5SDKM.js.map
|