@kokoa/clotho-editor 0.4.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.
@@ -1229,6 +1229,13 @@ body.studio-player-popout #clotho-player-preview {
1229
1229
  font-size: 0.66rem;
1230
1230
  color: var(--color-fg-muted);
1231
1231
  }
1232
+ /* A hint about something that will silently do nothing, rather than about taste. */
1233
+ .studio-props-warn {
1234
+ color: #b45309;
1235
+ background-color: color-mix(in srgb, #d97706 12%, transparent);
1236
+ border-radius: 4px;
1237
+ padding: 0.3rem 0.4rem;
1238
+ }
1232
1239
  .studio-camera-chips {
1233
1240
  display: flex;
1234
1241
  flex-wrap: wrap;
@@ -1682,6 +1689,24 @@ body.studio-player-popout #clotho-player-preview {
1682
1689
  font-size: 0.65rem;
1683
1690
  margin-right: 0.2rem;
1684
1691
  }
1692
+ /* One effect on the selected element, as a button that selects it. */
1693
+ .studio-effect-chip {
1694
+ display: block;
1695
+ width: 100%;
1696
+ text-align: left;
1697
+ margin-bottom: 0.25rem;
1698
+ padding: 0.28rem 0.4rem;
1699
+ border: 1px solid var(--color-border);
1700
+ border-radius: 4px;
1701
+ background-color: var(--color-surface);
1702
+ color: var(--color-fg);
1703
+ font-family: var(--font-mono);
1704
+ font-size: 0.66rem;
1705
+ cursor: pointer;
1706
+ }
1707
+ .studio-effect-chip:hover {
1708
+ border-color: var(--color-accent);
1709
+ }
1685
1710
  .studio-add-effect-bar {
1686
1711
  display: grid;
1687
1712
  grid-template-columns: 1fr 1fr auto;
@@ -2237,7 +2262,8 @@ body.studio-player-popout #clotho-player-preview {
2237
2262
  .studio-tl-element-track {
2238
2263
  position: relative;
2239
2264
  width: 100%;
2240
- height: 32px;
2265
+ /* Room for an effect strip under the appearance bars. */
2266
+ height: 42px;
2241
2267
  user-select: none;
2242
2268
  }
2243
2269
  .studio-tl-appearance {
@@ -2283,6 +2309,64 @@ body.studio-player-popout #clotho-player-preview {
2283
2309
  pointer-events: none;
2284
2310
  white-space: nowrap;
2285
2311
  }
2312
+ /*
2313
+ * Effects on the row of the element they decorate.
2314
+ *
2315
+ * They were nowhere on the timeline before, so an author scrubbing to find out
2316
+ * when something was emphasised had nothing to scrub against. A thin strip under
2317
+ * the appearance bars, because an effect is a note on an element's life rather
2318
+ * than a life of its own.
2319
+ */
2320
+ .studio-tl-effect {
2321
+ position: absolute;
2322
+ top: 31px;
2323
+ height: 9px;
2324
+ border-radius: 3px;
2325
+ background-color: color-mix(in srgb, #d97706 45%, var(--color-surface));
2326
+ border: 1px solid #d97706;
2327
+ cursor: pointer;
2328
+ display: flex;
2329
+ align-items: center;
2330
+ overflow: visible;
2331
+ }
2332
+ .studio-tl-effect.is-spotlight {
2333
+ background-color: color-mix(in srgb, #7c3aed 45%, var(--color-surface));
2334
+ border-color: #7c3aed;
2335
+ }
2336
+ .studio-tl-effect.is-trail {
2337
+ background-color: color-mix(in srgb, #0891b2 45%, var(--color-surface));
2338
+ border-color: #0891b2;
2339
+ }
2340
+ .studio-tl-effect.is-selected {
2341
+ outline: 2px solid var(--color-accent);
2342
+ outline-offset: 1px;
2343
+ }
2344
+ .studio-tl-effect-label {
2345
+ padding: 0 4px;
2346
+ pointer-events: none;
2347
+ white-space: nowrap;
2348
+ font-family: var(--font-mono);
2349
+ font-size: 0.58rem;
2350
+ line-height: 1;
2351
+ color: var(--color-fg);
2352
+ opacity: 0.85;
2353
+ }
2354
+ /*
2355
+ * How far a trail reaches back before its own start. `window` is the field
2356
+ * authors get wrong, and drawing it makes it a length rather than a number.
2357
+ */
2358
+ .studio-tl-effect-reach {
2359
+ position: absolute;
2360
+ top: 2px;
2361
+ bottom: 2px;
2362
+ border-radius: 3px 0 0 3px;
2363
+ background: repeating-linear-gradient(
2364
+ 90deg,
2365
+ color-mix(in srgb, #0891b2 40%, transparent) 0 3px,
2366
+ transparent 3px 6px
2367
+ );
2368
+ pointer-events: none;
2369
+ }
2286
2370
  .studio-tl-keyframe {
2287
2371
  position: absolute;
2288
2372
  top: 50%;
package/dist/index.js CHANGED
@@ -703,7 +703,7 @@ function StudioMount({
703
703
  if (repository) configureAnimationRepository(repository);
704
704
  let disposed = false;
705
705
  let cleanup;
706
- void import('./main-HDE5SDKM.js').then(({ initStudio }) => {
706
+ void import('./main-VRQTHHZE.js').then(({ initStudio }) => {
707
707
  if (disposed) return;
708
708
  cleanup = initStudio({
709
709
  initialId,
@@ -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, 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';
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";
@@ -2239,6 +2239,8 @@ function render2() {
2239
2239
  if (cameraFrame) canvasEl.appendChild(cameraFrame);
2240
2240
  const spotlightPreview = renderSpotlightPreview(def, snap2, elementsById);
2241
2241
  if (spotlightPreview) canvasEl.appendChild(spotlightPreview);
2242
+ const trailPreview = renderTrailPreview(def);
2243
+ if (trailPreview) canvasEl.appendChild(trailPreview);
2242
2244
  const selection = getSelection();
2243
2245
  if (selection.kind === "element") {
2244
2246
  const selEl = elementsById.get(selection.elementId);
@@ -2608,6 +2610,49 @@ function renderSpotlightPreview(def, snap2, elementsById) {
2608
2610
  g.appendChild(label);
2609
2611
  return g;
2610
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
+ }
2611
2656
  function cameraHandles(view, control) {
2612
2657
  const g = document.createElementNS(SVG_NS, "g");
2613
2658
  if (control.kind === "tracks") {
@@ -3253,6 +3298,7 @@ function renderInner() {
3253
3298
  <option value="pulse">pulse</option>
3254
3299
  <option value="flow">flow</option>
3255
3300
  <option value="spotlight">spotlight</option>
3301
+ <option value="trail">trail</option>
3256
3302
  </select>
3257
3303
  <button type="button" class="studio-btn" data-add-effect>\uFF0B \uD6A8\uACFC</button>
3258
3304
  </div>
@@ -3344,7 +3390,7 @@ function renderInner() {
3344
3390
  setSelection({ kind: "none" });
3345
3391
  return;
3346
3392
  }
3347
- const typeOpts = ["highlight", "pulse", "flow", "spotlight"].map(
3393
+ const typeOpts = ["highlight", "pulse", "flow", "spotlight", "trail"].map(
3348
3394
  (t) => `<option value="${t}" ${t === eff.type ? "selected" : ""}>${t}</option>`
3349
3395
  ).join("");
3350
3396
  if (eff.type === "spotlight") {
@@ -3375,6 +3421,36 @@ function renderInner() {
3375
3421
  `;
3376
3422
  return;
3377
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
+ }
3378
3454
  const elemOpts = def.elements.map(
3379
3455
  (e) => `<option value="${escapeHtml2(e.id)}" ${e.id === eff.elementId ? "selected" : ""}>${escapeHtml2(e.id)}</option>`
3380
3456
  ).join("");
@@ -3530,11 +3606,13 @@ function renderElementForm(def, el) {
3530
3606
  const baseHeader = `<span class="studio-props-header-title">${escapeHtml2(el.id)}</span><span class="studio-props-header-type">${escapeHtml2(el.type)}</span>`;
3531
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>`;
3532
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);
3533
3610
  panelEl.innerHTML = `
3534
3611
  ${timeHint}
3535
3612
  ${section("el-base", baseHeader, baseFields)}
3536
3613
  ${section("el-appearances", apHeader, appearances)}
3537
3614
  ${section("el-tracks", tracksHeader, tracks)}
3615
+ ${section("el-effects", `<span class="studio-props-header-title">\uD6A8\uACFC</span>`, effects)}
3538
3616
  ${section("el-bindings", `<span class="studio-props-header-title">\uB370\uC774\uD130 \uC5F0\uACB0 (${el.bindings.length})</span>`, bindings)}
3539
3617
  <div class="studio-props-empty" style="font-size:0.72rem;margin-top:0.5rem">
3540
3618
  base \uC18D\uC131\uC744 \uBCC0\uACBD\uD558\uBA74 \u2192 t=${getCurrentTime()} ms \uC5D0 keyframe \uCD94\uAC00<br/>
@@ -3542,6 +3620,25 @@ function renderElementForm(def, el) {
3542
3620
  </div>
3543
3621
  `;
3544
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
+ }
3545
3642
  function renderBindings(el) {
3546
3643
  const properties = bindablePropertiesFor(el);
3547
3644
  const rows = el.bindings.map(
@@ -4045,7 +4142,7 @@ function apply(key, value) {
4045
4142
  } else if (key.startsWith("effect.") && sel.kind === "effect") {
4046
4143
  const prop = key.slice(7);
4047
4144
  const patch = {};
4048
- if (prop === "time" || prop === "duration" || prop === "scale" || prop === "particles" || prop === "radius" || prop === "dim" || prop === "lit" || prop === "padding" || prop === "fadeIn") {
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") {
4049
4146
  patch[prop] = Number(value);
4050
4147
  } else {
4051
4148
  patch[prop] = value;
@@ -4053,6 +4150,94 @@ function apply(key, value) {
4053
4150
  updateEffect(sel.effectId, patch);
4054
4151
  }
4055
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
+ }
4056
4241
  function changeEffectType(def, effectId, type) {
4057
4242
  const current = def.effects.find((effect) => effect.id === effectId);
4058
4243
  if (!current || current.type === type) return;
@@ -4072,6 +4257,21 @@ function changeEffectType(def, effectId, type) {
4072
4257
  padding: 12,
4073
4258
  fadeIn: 200
4074
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
+ };
4075
4275
  } else {
4076
4276
  const single = {
4077
4277
  ...base,
@@ -4278,45 +4478,25 @@ function onClick2(e) {
4278
4478
  const typeSel = document.getElementById(
4279
4479
  "studio-new-effect-type"
4280
4480
  );
4281
- const type = typeSel?.value ?? "highlight";
4282
- const firstEl = def.elements[0];
4283
- if (!firstEl) return;
4284
- const id = uniqueEffectId();
4285
- const base = {
4286
- id,
4287
- elementId: firstEl.id,
4288
- time: getCurrentTime(),
4289
- duration: 500
4290
- };
4291
- let eff;
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")
4308
- eff = { ...base, type: "highlight", color: "#facc15" };
4309
- else if (type === "pulse") eff = { ...base, type: "pulse", scale: 1.12 };
4310
- else
4311
- eff = {
4312
- ...base,
4313
- type: "flow",
4314
- color: "#facc15",
4315
- particles: 3,
4316
- radius: 4,
4317
- duration: 800
4318
- };
4319
- 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
+ });
4320
4500
  return;
4321
4501
  }
4322
4502
  if (target.closest("[data-delete-effect]") && sel.kind === "effect") {
@@ -4545,7 +4725,7 @@ function render4() {
4545
4725
  </div>
4546
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>
4547
4727
  `;
4548
- renderElementTracks(def.elements, currentTime, totalPx, sel);
4728
+ renderElementTracks(def, currentTime, totalPx, sel);
4549
4729
  }
4550
4730
  function renderCameraRows(totalPx, sel) {
4551
4731
  const camera = getCamera();
@@ -4585,8 +4765,18 @@ function renderCameraRows(totalPx, sel) {
4585
4765
  function gutterLabel(el) {
4586
4766
  return `${friendlyElementLabel(el)} \xB7 ${el.type}`;
4587
4767
  }
4588
- function renderElementTracks(elements, currentTime, totalPx, sel) {
4768
+ function renderElementTracks(def, currentTime, totalPx, sel) {
4589
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
+ }
4590
4780
  if (elements.length === 0) {
4591
4781
  elTracksEl.innerHTML = '<p class="studio-tl-empty">\uC694\uC18C \uC5C6\uC74C</p>';
4592
4782
  return;
@@ -4620,12 +4810,24 @@ function renderElementTracks(elements, currentTime, totalPx, sel) {
4620
4810
  ).map(
4621
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>`
4622
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("");
4623
4824
  return `
4624
4825
  <div class="studio-tl-row studio-tl-element-row ${isSel ? "is-selected" : ""} ${el.type === "group" ? "is-group" : ""}" data-elem-id="${escapeHtml3(el.id)}">
4625
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>
4626
4827
  <div class="studio-tl-body" style="width:${totalPx}px">
4627
4828
  <div class="studio-tl-element-track" data-tl-area="elements">
4628
4829
  ${appearanceBars}
4830
+ ${effectBars}
4629
4831
  ${trackKfs}
4630
4832
  </div>
4631
4833
  </div>
@@ -4675,6 +4877,11 @@ function onElTracksClick(e) {
4675
4877
  if (dragMode) return;
4676
4878
  const target = e.target;
4677
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
+ }
4678
4885
  const row = target.closest("[data-elem-id]");
4679
4886
  if (row) {
4680
4887
  setSelection({ kind: "element", elementId: row.dataset.elemId });
@@ -18096,5 +18303,5 @@ function reflectState(ui) {
18096
18303
  }
18097
18304
 
18098
18305
  export { getVisibleElementIds, initStudio };
18099
- //# sourceMappingURL=main-HDE5SDKM.js.map
18100
- //# sourceMappingURL=main-HDE5SDKM.js.map
18306
+ //# sourceMappingURL=main-VRQTHHZE.js.map
18307
+ //# sourceMappingURL=main-VRQTHHZE.js.map