@kokoa/clotho-editor 0.3.0 → 0.4.1

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, 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';
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, 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, uniqueEffectId, addEffect, moveCameraKeyframe, childIdsOf, replaceEffect } from './chunk-WOP52KFW.js';
2
+ import { activeAppearance, animationDocumentSchema, bindablePropertiesFor, computeCamera, resolveAsset, buildScene, compileDataBindings, resolveBlendMode } from '@kokoa/clotho';
3
3
 
4
4
  // src/legacy/grid.ts
5
5
  var STORAGE_KEY = "studio.grid";
@@ -2237,6 +2237,10 @@ function render2() {
2237
2237
  }
2238
2238
  const cameraFrame = renderCameraFrame(def);
2239
2239
  if (cameraFrame) canvasEl.appendChild(cameraFrame);
2240
+ const spotlightPreview = renderSpotlightPreview(def, snap2, elementsById);
2241
+ if (spotlightPreview) canvasEl.appendChild(spotlightPreview);
2242
+ const trailPreview = renderTrailPreview(def);
2243
+ if (trailPreview) canvasEl.appendChild(trailPreview);
2240
2244
  const selection = getSelection();
2241
2245
  if (selection.kind === "element") {
2242
2246
  const selEl = elementsById.get(selection.elementId);
@@ -2548,6 +2552,107 @@ function moveCameraDrag(e) {
2548
2552
  updateCameraFocus(drag.focusIndex, { padding: next.padding });
2549
2553
  }
2550
2554
  }
2555
+ function renderSpotlightPreview(def, snap2, elementsById) {
2556
+ const selection = getSelection();
2557
+ if (selection.kind !== "effect") return null;
2558
+ const effect = def.effects.find(
2559
+ (candidate) => candidate.id === selection.effectId
2560
+ );
2561
+ if (!effect || effect.type !== "spotlight") return null;
2562
+ const boxes = effect.elementIds.map((id) => {
2563
+ const el = elementsById.get(id);
2564
+ const state = el ? snap2.get(id) : void 0;
2565
+ return el && state ? elementBBox(el, state) : null;
2566
+ }).filter(
2567
+ (box) => box !== null
2568
+ );
2569
+ if (boxes.length === 0) return null;
2570
+ const { width, height } = def.canvas;
2571
+ const pad = effect.padding;
2572
+ const g = document.createElementNS(SVG_NS, "g");
2573
+ g.setAttribute("class", "studio-spotlight-preview");
2574
+ g.setAttribute("pointer-events", "none");
2575
+ const holes = [];
2576
+ if (effect.shape === "elements") {
2577
+ for (const box of boxes) {
2578
+ holes.push(
2579
+ `M${box.x - pad} ${box.y - pad}H${box.x + box.w + pad}V${box.y + box.h + pad}H${box.x - pad}Z`
2580
+ );
2581
+ }
2582
+ } else {
2583
+ const x = Math.min(...boxes.map((box) => box.x)) - pad;
2584
+ const y = Math.min(...boxes.map((box) => box.y)) - pad;
2585
+ const right = Math.max(...boxes.map((box) => box.x + box.w)) + pad;
2586
+ const bottom = Math.max(...boxes.map((box) => box.y + box.h)) + pad;
2587
+ if (effect.shape === "circle") {
2588
+ const cx = (x + right) / 2;
2589
+ const cy = (y + bottom) / 2;
2590
+ const r = Math.hypot(right - x, bottom - y) / 2;
2591
+ holes.push(
2592
+ `M${cx - r} ${cy}a${r} ${r} 0 1 0 ${r * 2} 0a${r} ${r} 0 1 0 ${-r * 2} 0Z`
2593
+ );
2594
+ } else {
2595
+ holes.push(`M${x} ${y}H${right}V${bottom}H${x}Z`);
2596
+ }
2597
+ }
2598
+ const scrim = document.createElementNS(SVG_NS, "path");
2599
+ scrim.setAttribute("d", `M0 0H${width}V${height}H0Z ${holes.join(" ")}`);
2600
+ scrim.setAttribute("fill-rule", "evenodd");
2601
+ scrim.setAttribute("fill", effect.dimColor ?? "#0b1120");
2602
+ scrim.setAttribute("opacity", String(Math.max(effect.dim, 0.15)));
2603
+ g.appendChild(scrim);
2604
+ const label = document.createElementNS(SVG_NS, "text");
2605
+ label.setAttribute("x", "8");
2606
+ label.setAttribute("y", "18");
2607
+ label.setAttribute("class", "studio-spotlight-preview-label");
2608
+ const approximated = effect.shape === "elements" ? " \xB7 \uC2E4\uB8E8\uC5E3\uC740 \uC0C1\uC790\uB85C \uADFC\uC0AC" : "";
2609
+ label.textContent = `\u{1F526} ${effect.id} \xB7 ${effect.time}\u2013${effect.time + effect.duration}ms${approximated}`;
2610
+ g.appendChild(label);
2611
+ return g;
2612
+ }
2613
+ function renderTrailPreview(def) {
2614
+ const selection = getSelection();
2615
+ if (selection.kind !== "effect") return null;
2616
+ const effect = def.effects.find(
2617
+ (candidate) => candidate.id === selection.effectId
2618
+ );
2619
+ if (!effect || effect.type !== "trail") return null;
2620
+ const time = getCurrentTime();
2621
+ const g = document.createElementNS(SVG_NS, "g");
2622
+ g.setAttribute("class", "studio-trail-preview");
2623
+ g.setAttribute("pointer-events", "none");
2624
+ const label = document.createElementNS(SVG_NS, "text");
2625
+ label.setAttribute("x", "8");
2626
+ label.setAttribute("y", "18");
2627
+ label.setAttribute("class", "studio-spotlight-preview-label");
2628
+ const span = `${effect.time}\u2013${effect.time + effect.duration}ms`;
2629
+ if (time < effect.time || time >= effect.time + effect.duration) {
2630
+ label.textContent = `\u2733 ${effect.id} \xB7 \uC7AC\uC0DD \uC704\uCE58\uAC00 ${span} \uBC16\uC785\uB2C8\uB2E4`;
2631
+ g.appendChild(label);
2632
+ return g;
2633
+ }
2634
+ const pieces = buildScene(def, time, {}).nodes.filter(
2635
+ (node) => typeof node.key === "string" && node.key.startsWith(`${effect.id}-`)
2636
+ );
2637
+ if (pieces.length === 0) {
2638
+ label.textContent = `\u2733 ${effect.id} \xB7 \uCD5C\uADFC ${effect.window}ms \uB3D9\uC548 ${effect.elementId}\uAC00 \uC6C0\uC9C1\uC774\uC9C0 \uC54A\uC544 \uAF2C\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4`;
2639
+ g.appendChild(label);
2640
+ return g;
2641
+ }
2642
+ for (const piece of pieces) {
2643
+ const node = document.createElementNS(SVG_NS, piece.kind);
2644
+ for (const [name, value] of Object.entries(piece.attrs)) {
2645
+ if (value !== void 0 && value !== null) {
2646
+ node.setAttribute(name, String(value));
2647
+ }
2648
+ }
2649
+ g.appendChild(node);
2650
+ }
2651
+ const kind = pieces[0].kind === "circle" ? "\uC810" : "\uC120";
2652
+ label.textContent = `\u2733 ${effect.id} \xB7 window ${effect.window}ms \xB7 ${kind} ${pieces.length}\uAC1C (samples ${effect.samples})`;
2653
+ g.appendChild(label);
2654
+ return g;
2655
+ }
2551
2656
  function cameraHandles(view, control) {
2552
2657
  const g = document.createElementNS(SVG_NS, "g");
2553
2658
  if (control.kind === "tracks") {
@@ -3192,6 +3297,8 @@ function renderInner() {
3192
3297
  <option value="highlight">highlight</option>
3193
3298
  <option value="pulse">pulse</option>
3194
3299
  <option value="flow">flow</option>
3300
+ <option value="spotlight">spotlight</option>
3301
+ <option value="trail">trail</option>
3195
3302
  </select>
3196
3303
  <button type="button" class="studio-btn" data-add-effect>\uFF0B \uD6A8\uACFC</button>
3197
3304
  </div>
@@ -3283,12 +3390,70 @@ function renderInner() {
3283
3390
  setSelection({ kind: "none" });
3284
3391
  return;
3285
3392
  }
3393
+ const typeOpts = ["highlight", "pulse", "flow", "spotlight", "trail"].map(
3394
+ (t) => `<option value="${t}" ${t === eff.type ? "selected" : ""}>${t}</option>`
3395
+ ).join("");
3396
+ if (eff.type === "spotlight") {
3397
+ const shapeOpts = ["bbox", "circle", "elements"].map(
3398
+ (shape) => `<option value="${shape}" ${shape === eff.shape ? "selected" : ""}>${shape}${shape === "bbox" ? " (\uAE30\uBCF8)" : ""}</option>`
3399
+ ).join("");
3400
+ panelEl.innerHTML = `
3401
+ ${timeHint}
3402
+ <div class="studio-props-header"><span class="studio-props-header-title">${escapeHtml2(eff.id)}</span><span class="studio-props-header-type">spotlight</span></div>
3403
+ <label class="studio-field"><span>type</span><select data-prop-key="effect.type">${typeOpts}</select></label>
3404
+ <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>
3405
+ ${numberField("time (ms)", "effect.time", eff.time, 50)}
3406
+ ${numberField("duration (ms)", "effect.duration", eff.duration, 50)}
3407
+ ${numberField("fadeIn (ms) \xB7 \uC591 \uB05D\uC5D0 \uC801\uC6A9", "effect.fadeIn", eff.fadeIn, 50)}
3408
+ <div class="studio-camera-field-label">\uB300\uC0C1 \uC694\uC18C (${eff.elementIds.length})</div>
3409
+ ${elementCheckList(def, "data-spotlight-target", eff.id, eff.elementIds)}
3410
+ <label class="studio-field"><span>shape</span><select data-prop-key="effect.shape">${shapeOpts}</select></label>
3411
+ <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>
3412
+ ${rangeField("padding", "effect.padding", eff.padding, 0, 80, 1)}
3413
+ <div class="studio-props-header" style="margin-top:0.6rem"><span class="studio-props-header-title">\uAC00\uB824\uC9C0\uB294 \uCABD</span></div>
3414
+ ${rangeField("dim", "effect.dim", eff.dim, 0, 1, 0.01, "0\uC774\uBA74 \uC5B4\uB461\uAC8C \uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.")}
3415
+ ${colorField("dimColor", "effect.dimColor", eff.dimColor ?? "#0b1120")}
3416
+ <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>
3417
+ <div class="studio-props-header" style="margin-top:0.6rem"><span class="studio-props-header-title">\uBE44\uCD94\uB294 \uCABD</span></div>
3418
+ ${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.")}
3419
+ ${colorField("litColor", "effect.litColor", eff.litColor)}
3420
+ <button type="button" class="studio-btn studio-btn-danger" data-delete-effect style="margin-top:0.6rem">\u{1F5D1} \uD6A8\uACFC \uC0AD\uC81C</button>
3421
+ `;
3422
+ return;
3423
+ }
3424
+ if (eff.type === "trail") {
3425
+ const elemOptsTrail = def.elements.map(
3426
+ (e) => `<option value="${escapeHtml2(e.id)}" ${e.id === eff.elementId ? "selected" : ""}>${escapeHtml2(e.id)}</option>`
3427
+ ).join("");
3428
+ const resolved = trailModeHint(def, eff);
3429
+ panelEl.innerHTML = `
3430
+ ${timeHint}
3431
+ <div class="studio-props-header"><span class="studio-props-header-title">${escapeHtml2(eff.id)}</span><span class="studio-props-header-type">trail</span></div>
3432
+ <label class="studio-field"><span>type</span><select data-prop-key="effect.type">${typeOpts}</select></label>
3433
+ <p class="studio-props-empty" style="margin:0 0 0.4rem">\uC694\uC18C\uAC00 \uC9C0\uB098\uC628 \uC790\uB9AC\uB97C \uAF2C\uB9AC\uB85C \uB0A8\uAE41\uB2C8\uB2E4. \uC6C0\uC9C1\uC784 \uC790\uCCB4\uAC00 \uC815\uBCF4\uC778 \uBB38\uC11C\uC5D0\uC11C, \uC815\uC9C0 \uD504\uB808\uC784\uC774 \uC783\uC5B4\uBC84\uB9AC\uB294 \uAC83\uC744 \uB418\uB3CC\uB824 \uC90D\uB2C8\uB2E4.</p>
3434
+ <label class="studio-field"><span>elementId</span><select data-prop-key="effect.elementId">${elemOptsTrail}</select></label>
3435
+ ${trailMotionWarning(def, eff)}
3436
+ ${numberField("time (ms)", "effect.time", eff.time, 50)}
3437
+ ${numberField("duration (ms)", "effect.duration", eff.duration, 50)}
3438
+ <div class="studio-props-header" style="margin-top:0.6rem"><span class="studio-props-header-title">\uAF2C\uB9AC</span></div>
3439
+ ${rangeField("window (ms) \xB7 \uAF2C\uB9AC \uAE38\uC774", "effect.window", eff.window, 100, 5e3, 50, "\uC5BC\uB9C8\uB098 \uAC70\uC2AC\uB7EC \uC62C\uB77C\uAC08\uC9C0\uC785\uB2C8\uB2E4. \uD55C \uAC78\uC74C\uC758 \uBC15\uC790\uC5D0 \uB9DE\uCD94\uC138\uC694 \u2014 \uC774\uC0B0 \uCEE4\uC11C\uB294 \uD55C \uCE78, \uD6D1\uB294 \uC6C0\uC9C1\uC784\uC740 \uC870\uAE08 \uB354.")}
3440
+ ${rangeField("samples \xB7 \uD574\uC0C1\uB3C4", "effect.samples", eff.samples, 2, 32, 1, "\uCC3D\uC744 \uBA87 \uAC1C\uB85C \uB098\uB20C\uC9C0\uC785\uB2C8\uB2E4. \uC62C\uB824\uB3C4 \uAF2C\uB9AC\uAC00 \uAE38\uC5B4\uC9C0\uC9C0\uB294 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 \uAE38\uC774\uB294 window\uAC00 \uC815\uD569\uB2C8\uB2E4.")}
3441
+ ${selectField("mode", "effect.mode", eff.mode, [
3442
+ { value: "auto", label: "auto (\uBCF4\uAC04\uC73C\uB85C \uACB0\uC815)" },
3443
+ { value: "path", label: "path \u2014 \uC120" },
3444
+ { value: "dots", label: "dots \u2014 \uC810" }
3445
+ ])}
3446
+ <p class="studio-camera-hint">${resolved}</p>
3447
+ ${checkboxField("fade \xB7 \uC624\uB798\uB41C \uCABD\uC744 \uD750\uB9AC\uAC8C", "effect.fade", eff.fade)}
3448
+ ${colorField("color", "effect.color", eff.color)}
3449
+ ${rangeField("width", "effect.width", eff.width, 0.5, 16, 0.5, "path\uB294 \uC120\uC758 \uAD75\uAE30, dots\uB294 \uC810\uC758 \uBC18\uC9C0\uB984\uC785\uB2C8\uB2E4.")}
3450
+ <button type="button" class="studio-btn studio-btn-danger" data-delete-effect style="margin-top:0.6rem">\u{1F5D1} \uD6A8\uACFC \uC0AD\uC81C</button>
3451
+ `;
3452
+ return;
3453
+ }
3286
3454
  const elemOpts = def.elements.map(
3287
3455
  (e) => `<option value="${escapeHtml2(e.id)}" ${e.id === eff.elementId ? "selected" : ""}>${escapeHtml2(e.id)}</option>`
3288
3456
  ).join("");
3289
- const typeOpts = ["highlight", "pulse", "flow"].map(
3290
- (t) => `<option value="${t}" ${t === eff.type ? "selected" : ""}>${t}</option>`
3291
- ).join("");
3292
3457
  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);
3293
3458
  panelEl.innerHTML = `
3294
3459
  ${timeHint}
@@ -3313,13 +3478,13 @@ function rangeField(label, key, value, min, max, step, hint) {
3313
3478
  ${hint ? `<p class="studio-camera-hint">${escapeHtml2(hint)}</p>` : ""}
3314
3479
  </div>`;
3315
3480
  }
3316
- function focusTargetPicker(def, index, selected) {
3481
+ function elementCheckList(def, attribute, key, selected) {
3317
3482
  const chosen = new Set(selected);
3318
3483
  const rows = def.elements.map((element) => {
3319
3484
  const name = friendlyElementLabel(element);
3320
3485
  const same = name === element.id;
3321
3486
  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" : ""} />
3487
+ <input type="checkbox" ${attribute}="${escapeHtml2(key)}" value="${escapeHtml2(element.id)}" ${chosen.has(element.id) ? "checked" : ""} />
3323
3488
  <span class="studio-camera-target-name">${escapeHtml2(name)}</span>
3324
3489
  ${same ? "" : `<span class="studio-camera-target-id">${escapeHtml2(element.id)}</span>`}
3325
3490
  <span class="studio-camera-target-type">${element.type}</span>
@@ -3366,7 +3531,7 @@ function renderCameraForm(def, focusIndex) {
3366
3531
  </div>
3367
3532
  </div>
3368
3533
  <div class="studio-camera-field-label">\uB300\uC0C1 \uC694\uC18C (${entry.elementIds.length})</div>
3369
- ${focusTargetPicker(def, index, entry.elementIds)}
3534
+ ${elementCheckList(def, "data-camera-target", String(index), entry.elementIds)}
3370
3535
  ${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
3536
  ${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
3537
  <button type="button" class="studio-btn studio-btn-danger" data-delete-camera-focus="${index}">\u{1F5D1} focus \uC0AD\uC81C</button>
@@ -3441,11 +3606,13 @@ function renderElementForm(def, el) {
3441
3606
  const baseHeader = `<span class="studio-props-header-title">${escapeHtml2(el.id)}</span><span class="studio-props-header-type">${escapeHtml2(el.type)}</span>`;
3442
3607
  const apHeader = `<span class="studio-props-header-title">\uCD9C\uD604 (Appearances)</span><button type="button" class="studio-btn studio-btn-small" data-add-appearance>\uFF0B</button>`;
3443
3608
  const tracksHeader = `<span class="studio-props-header-title">\uD0A4\uD504\uB808\uC784 \uD2B8\uB799 (${el.tracks.length})</span>`;
3609
+ const effects = renderElementEffects(def, el);
3444
3610
  panelEl.innerHTML = `
3445
3611
  ${timeHint}
3446
3612
  ${section("el-base", baseHeader, baseFields)}
3447
3613
  ${section("el-appearances", apHeader, appearances)}
3448
3614
  ${section("el-tracks", tracksHeader, tracks)}
3615
+ ${section("el-effects", `<span class="studio-props-header-title">\uD6A8\uACFC</span>`, effects)}
3449
3616
  ${section("el-bindings", `<span class="studio-props-header-title">\uB370\uC774\uD130 \uC5F0\uACB0 (${el.bindings.length})</span>`, bindings)}
3450
3617
  <div class="studio-props-empty" style="font-size:0.72rem;margin-top:0.5rem">
3451
3618
  base \uC18D\uC131\uC744 \uBCC0\uACBD\uD558\uBA74 \u2192 t=${getCurrentTime()} ms \uC5D0 keyframe \uCD94\uAC00<br/>
@@ -3453,6 +3620,25 @@ function renderElementForm(def, el) {
3453
3620
  </div>
3454
3621
  `;
3455
3622
  }
3623
+ function renderElementEffects(def, el) {
3624
+ const mine = def.effects.filter(
3625
+ (effect) => effect.type === "spotlight" ? effect.elementIds.includes(el.id) : effect.elementId === el.id
3626
+ );
3627
+ const rows = mine.map(
3628
+ (effect) => `<button type="button" class="studio-effect-chip" data-select-effect="${escapeHtml2(effect.id)}">${escapeHtml2(effect.type)} \xB7 ${effect.time}\u2013${effect.time + effect.duration}ms</button>`
3629
+ ).join("");
3630
+ return `${rows || '<p class="studio-props-empty" style="font-size:0.72rem">\uC774 \uC694\uC18C\uC5D0 \uAC78\uB9B0 \uD6A8\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</p>'}
3631
+ <div class="studio-add-effect-bar">
3632
+ <select data-new-effect-for="${escapeHtml2(el.id)}">
3633
+ <option value="highlight">highlight</option>
3634
+ <option value="pulse">pulse</option>
3635
+ <option value="flow">flow</option>
3636
+ <option value="spotlight">spotlight</option>
3637
+ <option value="trail">trail</option>
3638
+ </select>
3639
+ <button type="button" class="studio-btn" data-add-effect-for="${escapeHtml2(el.id)}">\uFF0B \uD6A8\uACFC</button>
3640
+ </div>`;
3641
+ }
3456
3642
  function renderBindings(el) {
3457
3643
  const properties = bindablePropertiesFor(el);
3458
3644
  const rows = el.bindings.map(
@@ -3697,6 +3883,11 @@ function onInput(e) {
3697
3883
  toggleFocusTarget(Number(focusTarget), target.value, target.checked);
3698
3884
  return;
3699
3885
  }
3886
+ const spotTarget = target.dataset.spotlightTarget;
3887
+ if (spotTarget !== void 0 && target instanceof HTMLInputElement) {
3888
+ toggleSpotlightTarget(spotTarget, target.value, target.checked);
3889
+ return;
3890
+ }
3700
3891
  const key = target.dataset.propKey;
3701
3892
  if (!key) return;
3702
3893
  if (target.type === "text" && e.isComposing) return;
@@ -3946,10 +4137,12 @@ function apply(key, value) {
3946
4137
  if (targets) references[token] = targets;
3947
4138
  else delete references[token];
3948
4139
  updateChapter(sel.chapterId, { references });
4140
+ } else if (key === "effect.type" && sel.kind === "effect") {
4141
+ changeEffectType(def, sel.effectId, String(value));
3949
4142
  } else if (key.startsWith("effect.") && sel.kind === "effect") {
3950
4143
  const prop = key.slice(7);
3951
4144
  const patch = {};
3952
- if (prop === "time" || prop === "duration" || prop === "scale" || prop === "particles" || prop === "radius") {
4145
+ if (prop === "time" || prop === "duration" || prop === "scale" || prop === "particles" || prop === "radius" || prop === "dim" || prop === "lit" || prop === "padding" || prop === "fadeIn" || prop === "window" || prop === "samples" || prop === "width") {
3953
4146
  patch[prop] = Number(value);
3954
4147
  } else {
3955
4148
  patch[prop] = value;
@@ -3957,6 +4150,155 @@ function apply(key, value) {
3957
4150
  updateEffect(sel.effectId, patch);
3958
4151
  }
3959
4152
  }
4153
+ var TRAIL_POSITION_PROPERTIES = /* @__PURE__ */ new Set([
4154
+ "x",
4155
+ "y",
4156
+ "cx",
4157
+ "cy",
4158
+ "x1",
4159
+ "y1",
4160
+ "x2",
4161
+ "y2"
4162
+ ]);
4163
+ function trailPositionTracks(def, elementId) {
4164
+ const el = def.elements.find((candidate) => candidate.id === elementId);
4165
+ return (el?.tracks ?? []).filter(
4166
+ (track) => TRAIL_POSITION_PROPERTIES.has(track.property)
4167
+ );
4168
+ }
4169
+ function trailModeHint(def, effect) {
4170
+ if (effect.mode === "path") return "\uC0D8\uD50C\uC744 \uC120\uC73C\uB85C \uC787\uC2B5\uB2C8\uB2E4.";
4171
+ if (effect.mode === "dots") return "\uC0D8\uD50C\uB9C8\uB2E4 \uC810\uC744 \uCC0D\uC2B5\uB2C8\uB2E4.";
4172
+ const tracks = trailPositionTracks(def, effect.elementId);
4173
+ if (tracks.length === 0) return "\uC704\uCE58 track\uC774 \uC5C6\uC73C\uBBC0\uB85C \uC120\uC73C\uB85C \uADF8\uB9BD\uB2C8\uB2E4.";
4174
+ const steps = tracks.every(
4175
+ (track) => resolveBlendMode(track.interpolate, track.property) === "discrete"
4176
+ );
4177
+ return steps ? "auto \u2192 \uC810. \uC704\uCE58 track\uC774 \uC774\uC0B0 \uBCF4\uAC04\uC774\uB77C \uC9C0\uB098\uC9C0 \uC54A\uC740 \uACF3\uC744 \uC787\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." : "auto \u2192 \uC120. \uC704\uCE58\uAC00 \uC5F0\uC18D\uC73C\uB85C \uBCC0\uD558\uBBC0\uB85C \uC0D8\uD50C\uC744 \uC774\uC5B4\uB3C4 \uAC70\uC9D3\uC774 \uC544\uB2D9\uB2C8\uB2E4.";
4178
+ }
4179
+ function trailMotionWarning(def, effect) {
4180
+ const el = def.elements.find(
4181
+ (candidate) => candidate.id === effect.elementId
4182
+ );
4183
+ if (!el) return "";
4184
+ if (trailPositionTracks(def, effect.elementId).length > 0) return "";
4185
+ const inGroup = el.parentId !== void 0;
4186
+ return `<p class="studio-camera-hint studio-props-warn">\u26A0 <code>${escapeHtml2(effect.elementId)}</code>\uC5D0 \uC704\uCE58 track\uC774 \uC5C6\uC2B5\uB2C8\uB2E4${inGroup ? " (\uBD80\uBAA8 group\uC774 \uC6C0\uC9C1\uC778\uB2E4\uBA74 \uAF2C\uB9AC\uB294 \uB0A8\uC2B5\uB2C8\uB2E4)" : ""}. \uC6C0\uC9C1\uC774\uC9C0 \uC54A\uB294 \uC694\uC18C\uB294 \uAF2C\uB9AC\uB97C \uB0A8\uAE30\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.</p>`;
4187
+ }
4188
+ function createEffect(def, type, seeds) {
4189
+ const firstEl = def.elements[0];
4190
+ if (!firstEl) return;
4191
+ const targets = seeds.length > 0 ? seeds : [firstEl.id];
4192
+ const id = uniqueEffectId();
4193
+ const time = getCurrentTime();
4194
+ const base = { id, elementId: targets[0], time, duration: 500 };
4195
+ let effect;
4196
+ if (type === "spotlight") {
4197
+ effect = {
4198
+ id,
4199
+ type: "spotlight",
4200
+ elementIds: [...targets],
4201
+ time,
4202
+ duration: 1200,
4203
+ dim: 0.7,
4204
+ lit: 0,
4205
+ litColor: "#fde68a",
4206
+ shape: "bbox",
4207
+ padding: 12,
4208
+ fadeIn: 200
4209
+ };
4210
+ } else if (type === "trail") {
4211
+ effect = {
4212
+ id,
4213
+ type: "trail",
4214
+ elementId: targets[0],
4215
+ time,
4216
+ duration: Math.max(1e3, def.duration - time),
4217
+ window: 1200,
4218
+ samples: 12,
4219
+ mode: "auto",
4220
+ fade: true,
4221
+ color: "#94a3b8",
4222
+ width: 2
4223
+ };
4224
+ } else if (type === "pulse") {
4225
+ effect = { ...base, type: "pulse", scale: 1.12 };
4226
+ } else if (type === "flow") {
4227
+ effect = {
4228
+ ...base,
4229
+ type: "flow",
4230
+ color: "#facc15",
4231
+ particles: 3,
4232
+ radius: 4,
4233
+ duration: 800
4234
+ };
4235
+ } else {
4236
+ effect = { ...base, type: "highlight", color: "#facc15" };
4237
+ }
4238
+ addEffect(effect);
4239
+ setSelection({ kind: "effect", effectId: id });
4240
+ }
4241
+ function changeEffectType(def, effectId, type) {
4242
+ const current = def.effects.find((effect) => effect.id === effectId);
4243
+ if (!current || current.type === type) return;
4244
+ const targets = current.type === "spotlight" ? current.elementIds : [current.elementId];
4245
+ const base = { id: current.id, time: current.time };
4246
+ let next;
4247
+ if (type === "spotlight") {
4248
+ next = {
4249
+ ...base,
4250
+ type: "spotlight",
4251
+ elementIds: targets,
4252
+ duration: Math.max(current.duration, 800),
4253
+ dim: 0.7,
4254
+ lit: 0,
4255
+ litColor: "#fde68a",
4256
+ shape: "bbox",
4257
+ padding: 12,
4258
+ fadeIn: 200
4259
+ };
4260
+ } else if (type === "trail") {
4261
+ next = {
4262
+ ...base,
4263
+ type: "trail",
4264
+ elementId: targets[0] ?? "",
4265
+ // A trail needs long enough to be seen moving; the emphasis effects default
4266
+ // to half a second, which is shorter than most single steps.
4267
+ duration: Math.max(current.duration, 3e3),
4268
+ window: 1200,
4269
+ samples: 12,
4270
+ mode: "auto",
4271
+ fade: true,
4272
+ color: "#94a3b8",
4273
+ width: 2
4274
+ };
4275
+ } else {
4276
+ const single = {
4277
+ ...base,
4278
+ elementId: targets[0] ?? "",
4279
+ duration: current.duration
4280
+ };
4281
+ next = type === "pulse" ? { ...single, type: "pulse", scale: 1.12 } : type === "flow" ? {
4282
+ ...single,
4283
+ type: "flow",
4284
+ color: "#facc15",
4285
+ particles: 3,
4286
+ radius: 4
4287
+ } : { ...single, type: "highlight", color: "#facc15" };
4288
+ }
4289
+ replaceEffect(effectId, next);
4290
+ }
4291
+ function toggleSpotlightTarget(effectId, id, on) {
4292
+ const def = getDef();
4293
+ const effect = def?.effects.find((candidate) => candidate.id === effectId);
4294
+ if (!effect || effect.type !== "spotlight") return;
4295
+ const next = on ? [...effect.elementIds, id] : effect.elementIds.filter((target) => target !== id);
4296
+ if (next.length === 0) {
4297
+ render3();
4298
+ return;
4299
+ }
4300
+ updateEffect(effectId, { elementIds: next });
4301
+ }
3960
4302
  function parseBindingFallback(value) {
3961
4303
  if (value === "") return void 0;
3962
4304
  if (value === "true") return true;
@@ -4136,30 +4478,25 @@ function onClick2(e) {
4136
4478
  const typeSel = document.getElementById(
4137
4479
  "studio-new-effect-type"
4138
4480
  );
4139
- const type = typeSel?.value ?? "highlight";
4140
- const firstEl = def.elements[0];
4141
- if (!firstEl) return;
4142
- const id = uniqueEffectId();
4143
- const base = {
4144
- id,
4145
- elementId: firstEl.id,
4146
- time: getCurrentTime(),
4147
- duration: 500
4148
- };
4149
- let eff;
4150
- if (type === "highlight")
4151
- eff = { ...base, type: "highlight", color: "#facc15" };
4152
- else if (type === "pulse") eff = { ...base, type: "pulse", scale: 1.12 };
4153
- else
4154
- eff = {
4155
- ...base,
4156
- type: "flow",
4157
- color: "#facc15",
4158
- particles: 3,
4159
- radius: 4,
4160
- duration: 800
4161
- };
4162
- addEffect(eff);
4481
+ const seeds = sel.kind === "element" ? [sel.elementId] : sel.kind === "elements" ? [...sel.elementIds] : [];
4482
+ createEffect(def, typeSel?.value ?? "highlight", seeds);
4483
+ return;
4484
+ }
4485
+ const addFor = target.closest("[data-add-effect-for]");
4486
+ if (addFor) {
4487
+ const elementId = addFor.dataset.addEffectFor;
4488
+ const typeSel = panelEl?.querySelector(
4489
+ `[data-new-effect-for="${CSS.escape(elementId)}"]`
4490
+ );
4491
+ createEffect(def, typeSel?.value ?? "highlight", [elementId]);
4492
+ return;
4493
+ }
4494
+ const pickEffect = target.closest("[data-select-effect]");
4495
+ if (pickEffect) {
4496
+ setSelection({
4497
+ kind: "effect",
4498
+ effectId: pickEffect.dataset.selectEffect
4499
+ });
4163
4500
  return;
4164
4501
  }
4165
4502
  if (target.closest("[data-delete-effect]") && sel.kind === "effect") {
@@ -4388,7 +4725,7 @@ function render4() {
4388
4725
  </div>
4389
4726
  <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>
4390
4727
  `;
4391
- renderElementTracks(def.elements, currentTime, totalPx, sel);
4728
+ renderElementTracks(def, currentTime, totalPx, sel);
4392
4729
  }
4393
4730
  function renderCameraRows(totalPx, sel) {
4394
4731
  const camera = getCamera();
@@ -4428,8 +4765,18 @@ function renderCameraRows(totalPx, sel) {
4428
4765
  function gutterLabel(el) {
4429
4766
  return `${friendlyElementLabel(el)} \xB7 ${el.type}`;
4430
4767
  }
4431
- function renderElementTracks(elements, currentTime, totalPx, sel) {
4768
+ function renderElementTracks(def, currentTime, totalPx, sel) {
4432
4769
  if (!elTracksEl) return;
4770
+ const elements = def.elements;
4771
+ const effectsByElement = /* @__PURE__ */ new Map();
4772
+ for (const effect of def.effects) {
4773
+ const targets = effect.type === "spotlight" ? effect.elementIds : [effect.elementId];
4774
+ for (const target of targets) {
4775
+ const bucket = effectsByElement.get(target);
4776
+ if (bucket) bucket.push(effect);
4777
+ else effectsByElement.set(target, [effect]);
4778
+ }
4779
+ }
4433
4780
  if (elements.length === 0) {
4434
4781
  elTracksEl.innerHTML = '<p class="studio-tl-empty">\uC694\uC18C \uC5C6\uC74C</p>';
4435
4782
  return;
@@ -4463,12 +4810,24 @@ function renderElementTracks(elements, currentTime, totalPx, sel) {
4463
4810
  ).map(
4464
4811
  (kf) => `<div class="studio-tl-keyframe" style="left:${timeToPx(kf.time)}px" data-kf-elem-id="${escapeHtml3(el.id)}" data-kf-prop="${escapeHtml3(kf.prop)}" data-kf-time="${kf.time}" title="${escapeHtml3(kf.prop)} @ ${kf.time}ms (\uB4DC\uB798\uADF8\uB85C \uC774\uB3D9)">\u25C6</div>`
4465
4812
  ).join("");
4813
+ const effectBars = (effectsByElement.get(el.id) ?? []).map((effect) => {
4814
+ const left = timeToPx(effect.time);
4815
+ const width = Math.max(6, timeToPx(effect.duration));
4816
+ const selected = sel.kind === "effect" && sel.effectId === effect.id;
4817
+ const trailWindow = effect.type === "trail" ? effect.window : 0;
4818
+ const lead = Math.min(timeToPx(trailWindow), left);
4819
+ const reach = lead > 0 ? `<span class="studio-tl-effect-reach" style="left:${-lead}px;width:${lead}px" title="window ${trailWindow}ms"></span>` : "";
4820
+ return `<div class="studio-tl-effect ${selected ? "is-selected" : ""} is-${escapeHtml3(effect.type)}" style="left:${left}px;width:${width}px" data-effect-id="${escapeHtml3(effect.id)}" title="${escapeHtml3(effect.type)} ${escapeHtml3(effect.id)} \xB7 ${effect.time}-${effect.time + effect.duration}ms">
4821
+ ${reach}<span class="studio-tl-effect-label">${escapeHtml3(effect.type)}</span>
4822
+ </div>`;
4823
+ }).join("");
4466
4824
  return `
4467
4825
  <div class="studio-tl-row studio-tl-element-row ${isSel ? "is-selected" : ""} ${el.type === "group" ? "is-group" : ""}" data-elem-id="${escapeHtml3(el.id)}">
4468
4826
  <div class="studio-tl-gutter studio-tl-element-label" style="--studio-tree-depth:${depth}" title="${escapeHtml3(el.id)}">${el.type === "group" ? "\u25BE " : depth > 0 ? "\u21B3 " : ""}${escapeHtml3(gutterLabel(el))}</div>
4469
4827
  <div class="studio-tl-body" style="width:${totalPx}px">
4470
4828
  <div class="studio-tl-element-track" data-tl-area="elements">
4471
4829
  ${appearanceBars}
4830
+ ${effectBars}
4472
4831
  ${trackKfs}
4473
4832
  </div>
4474
4833
  </div>
@@ -4518,6 +4877,11 @@ function onElTracksClick(e) {
4518
4877
  if (dragMode) return;
4519
4878
  const target = e.target;
4520
4879
  if (target.closest("[data-ap-idx]") || target.closest("[data-edge]")) return;
4880
+ const effectBar = target.closest("[data-effect-id]");
4881
+ if (effectBar) {
4882
+ setSelection({ kind: "effect", effectId: effectBar.dataset.effectId });
4883
+ return;
4884
+ }
4521
4885
  const row = target.closest("[data-elem-id]");
4522
4886
  if (row) {
4523
4887
  setSelection({ kind: "element", elementId: row.dataset.elemId });
@@ -17939,5 +18303,5 @@ function reflectState(ui) {
17939
18303
  }
17940
18304
 
17941
18305
  export { getVisibleElementIds, initStudio };
17942
- //# sourceMappingURL=main-DC6D6RNQ.js.map
17943
- //# sourceMappingURL=main-DC6D6RNQ.js.map
18306
+ //# sourceMappingURL=main-VRQTHHZE.js.map
18307
+ //# sourceMappingURL=main-VRQTHHZE.js.map