@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.
@@ -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, setCurrentTime, addAppearance, jumpBack, jumpForward, getHistory, promoteDraftToSaved, toggleSelectionFor, moveGroupBy, placeholderImageUrl, groupBbox, layoutIdsFor, findLayoutCollisions, updateLocales, updateData, updateResponsive, updateDuration, updateCheckpoint, updateAppearance, updateEffect, childIdsOf } from './chunk-FAI2G3XK.js';
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
- 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) {
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-SKM3P4X7.js.map
17376
- //# sourceMappingURL=main-SKM3P4X7.js.map
17942
+ //# sourceMappingURL=main-DC6D6RNQ.js.map
17943
+ //# sourceMappingURL=main-DC6D6RNQ.js.map