Movie Editor: Effects dropdown on the timeline toolbar

'✨ Effects ▾' on the selected clip: pick an effect/transition (zoom in/out, blur, fade
in/out, crossfade/fade-to-black/wipe → next), enter its value (strength/seconds/frames)
where relevant, and Add. Writes onto the scene, so the inspector's per-scene controls
remain the single source of truth and reflect whatever is added here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-08 00:39:54 +03:00
co-authored by Claude Opus 4.8
parent 4e9d0e4c1d
commit 6eab8d9632
2 changed files with 74 additions and 0 deletions
+10
View File
@@ -377,6 +377,16 @@ input[type=number] { font-family: var(--font-mono); }
body.tl-reordering { cursor: grabbing; user-select: none; }
.tl-dropline { position: absolute; top: 6px; bottom: 6px; width: 2px; background: var(--amber, #e0a23a);
box-shadow: 0 0 6px 1px rgba(224,162,58,.8); z-index: 60; pointer-events: none; }
.tl-dd { position: relative; display: inline-block; }
.tl-dd-panel { position: absolute; top: calc(100% + 4px); left: 0; z-index: 200; min-width: 200px;
background: #1b1812; border: 1px solid var(--line); border-radius: 8px; padding: 8px;
display: flex; flex-direction: column; gap: 7px; box-shadow: 0 8px 22px rgba(0,0,0,.55); }
.tl-dd-panel[hidden] { display: none; }
.tl-dd-head { font-size: 11px; color: var(--muted, #9a8f78); text-transform: uppercase; letter-spacing: .04em; }
.tl-dd-sel { width: 100%; }
.tl-dd-val { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 12px; }
.tl-dd-val[hidden] { display: none; }
.tl-dd-val input { width: 78px; }
.clip.selected { border-color: var(--teal); box-shadow: inset 0 0 0 1px var(--teal-soft); z-index: 3; }
.clip.selected::before { background: var(--teal); opacity: 1; }
.clip.excluded { opacity: .42; }
+64
View File
@@ -231,6 +231,69 @@
return seam;
}
// ── effects / transitions menu (timeline toolbar) ───────────────────────────────
// Each writes onto the selected scene, so the inspector's per-scene controls stay the
// single source of truth and reflect whatever is added here.
const FX_DEFS = [
{ id: "zoom_in", label: "Zoom in (crop)", apply: (sc) => ({ effects: { ...(sc.effects || {}), zoom: "in" } }) },
{ id: "zoom_out", label: "Zoom out (pad)", apply: (sc) => ({ effects: { ...(sc.effects || {}), zoom: "out" } }) },
{ id: "blur", label: "Gaussian blur", val: { label: "Strength (0–1)", def: 0.3, min: 0, max: 1, step: 0.05 }, apply: (sc, v) => ({ effects: { ...(sc.effects || {}), blur: v } }) },
{ id: "fade_in", label: "Fade in", val: { label: "Seconds", def: 0.5, min: 0, max: 10, step: 0.1 }, apply: (sc, v) => ({ effects: { ...(sc.effects || {}), fade_in: v } }) },
{ id: "fade_out", label: "Fade out", val: { label: "Seconds", def: 0.5, min: 0, max: 10, step: 0.1 }, apply: (sc, v) => ({ effects: { ...(sc.effects || {}), fade_out: v } }) },
{ id: "crossfade", label: "Crossfade → next", val: { label: "Frames", def: 16, min: 1, max: 120, step: 1 }, apply: (sc, v) => ({ video_transition: "crossfade", transition_frames: Math.round(v) }) },
{ id: "fadeblack", label: "Fade to black → next", val: { label: "Frames", def: 16, min: 1, max: 120, step: 1 }, apply: (sc, v) => ({ video_transition: "fadeblack", transition_frames: Math.round(v) }) },
{ id: "wipeleft", label: "Wipe left → next", val: { label: "Frames", def: 16, min: 1, max: 120, step: 1 }, apply: (sc, v) => ({ video_transition: "wipeleft", transition_frames: Math.round(v) }) },
{ id: "wiperight", label: "Wipe right → next", val: { label: "Frames", def: 16, min: 1, max: 120, step: 1 }, apply: (sc, v) => ({ video_transition: "wiperight", transition_frames: Math.round(v) }) },
];
function effectsDropdown(st) {
const wrap = el("div", "tl-dd");
const hasSel = !!st.selectedSceneId;
const btn = el("button", "btn ghost tiny", "✨ Effects ▾");
btn.disabled = !hasSel;
btn.title = hasSel ? "Add a video effect or transition to the selected clip" : "Select a clip first";
wrap.append(btn);
const panel = el("div", "tl-dd-panel"); panel.hidden = true;
panel.append(el("div", "tl-dd-head", "Add to selected clip"));
const sel = el("select", "tl-dd-sel");
FX_DEFS.forEach((d) => sel.append(new Option(d.label, d.id)));
const valRow = el("label", "tl-dd-val");
const valLbl = el("span", null, ""); const valIn = el("input"); valIn.type = "number";
valRow.append(valLbl, valIn);
const addBtn = el("button", "btn primary tiny", "Add");
const sync = () => {
const d = FX_DEFS.find((x) => x.id === sel.value);
if (d && d.val) {
valRow.hidden = false; valLbl.textContent = d.val.label;
valIn.min = d.val.min; valIn.max = d.val.max; valIn.step = d.val.step;
if (valIn.value === "") valIn.value = d.val.def;
} else valRow.hidden = true;
};
sel.onchange = () => { valIn.value = ""; sync(); };
addBtn.onclick = () => {
const d = FX_DEFS.find((x) => x.id === sel.value); if (!d) return;
const sc = S.scene(st.selectedSceneId); if (!sc) { alert("Select a clip first."); return; }
const v = d.val ? parseFloat(valIn.value || d.val.def) : null;
S.patchScene(sc.id, d.apply(sc, v));
panel.hidden = true;
};
panel.append(sel, valRow, addBtn);
sync();
wrap.append(panel);
btn.onclick = (e) => {
e.stopPropagation();
const show = panel.hidden;
panel.hidden = !show;
if (show) {
const off = (ev) => { if (!wrap.contains(ev.target)) { panel.hidden = true; document.removeEventListener("mousedown", off, true); } };
setTimeout(() => document.addEventListener("mousedown", off, true), 0);
}
};
return wrap;
}
// ── toolbar ─────────────────────────────────────────────────────────────────────
function toolbar(st, p, totalSec) {
const bar = el("div", "tl-toolbar");
@@ -254,6 +317,7 @@
exp.disabled = !(hasSel && hasRender(st, st.selectedSceneId));
exp.onclick = () => S.exportSelected();
bar.append(split); bar.append(del); bar.append(exp);
bar.append(effectsDropdown(st));
// Scene rating — only when FunPack Studio is the conditioning provider AND the
// selected clip has a render. Rates that clip's own scene (cut-aware by scene id);