Movie Editor: i2v anchors drop on the Plan, not the timeline
The timeline is the rendered result, so setting up a generation must not touch it. Removed the clip-level anchor drop and the timeline track's image->anchored-scene drop. Image media now drops onto Plan cards (sets that scene's i2v anchor) or onto empty Plan space (creates a new anchored scene). Video clips (finished results) still drop on the timeline track. Plan-only image filter; teal drop-target hint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4f562cf0ff
commit
93c8068b90
@@ -60,6 +60,24 @@
|
||||
const l = el("label", "plan-field"); l.append(el("span", null, labelText)); l.append(control); return l;
|
||||
}
|
||||
|
||||
// i2v anchors are dropped on the PLAN (never the timeline). Accept image media only.
|
||||
function attachAnchorDrop(node, st, onImage) {
|
||||
node.addEventListener("dragover", (e) => {
|
||||
if (!e.dataTransfer.types.includes("application/funpack-media")) return;
|
||||
e.preventDefault(); e.dataTransfer.dropEffect = "copy"; node.classList.add("drop-target");
|
||||
});
|
||||
node.addEventListener("dragleave", () => node.classList.remove("drop-target"));
|
||||
node.addEventListener("drop", (e) => {
|
||||
node.classList.remove("drop-target");
|
||||
const id = e.dataTransfer.getData("application/funpack-media");
|
||||
if (!id) return;
|
||||
const asset = (st.mediaBin || []).find((m) => m.id === id);
|
||||
if (!asset || asset.kind !== "image") return; // images = anchors; videos go to the timeline
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
onImage(id);
|
||||
});
|
||||
}
|
||||
|
||||
function cardEditor(st, sc) {
|
||||
const root = rootOf(sc);
|
||||
const ed = el("div", "plan-editor");
|
||||
@@ -152,6 +170,7 @@
|
||||
chev.onclick = (e) => { e.stopPropagation(); expandedId = isOpen ? null : sc.id; S.selectScene(sc.id); render(st); };
|
||||
c.append(chev);
|
||||
}
|
||||
if (!S.isVideoClip(sc)) attachAnchorDrop(c, st, (id) => S.assignMediaToScene(sc.id, id));
|
||||
c.onclick = () => { if (st.selectedSceneId !== sc.id) S.selectScene(sc.id); };
|
||||
return c;
|
||||
}
|
||||
@@ -166,9 +185,11 @@
|
||||
const rowEl = el("div", "plan-row");
|
||||
scenes.forEach((sc, i) => rowEl.append(cardEl(st, sc, i)));
|
||||
const add = el("button", "plan-add", "+ Add scene");
|
||||
add.title = "Add a new scene to the plan";
|
||||
add.title = "Add a new scene to the plan · or drop an image here to anchor a new scene";
|
||||
add.onclick = () => S.addScene();
|
||||
rowEl.append(add);
|
||||
// Drop an image on empty plan space → a new scene anchored to it (cards stopPropagation).
|
||||
attachAnchorDrop(rowEl, st, (id) => S.addImageClip(id));
|
||||
body.append(rowEl);
|
||||
if (expandedId) {
|
||||
const sc = S.scene(expandedId);
|
||||
|
||||
@@ -1426,6 +1426,7 @@ body.tour-mode { overflow: hidden; }
|
||||
}
|
||||
.plan-card:hover { border-color: color-mix(in srgb, var(--amber) 50%, var(--line)); }
|
||||
.plan-card.sel, .plan-card.open { border-color: var(--amber); }
|
||||
.plan-card.drop-target, .plan-row.drop-target { outline: 2px dashed var(--teal); outline-offset: -2px; border-radius: 8px; }
|
||||
.plan-thumb { width: 40px; height: 40px; flex: 0 0 40px; border-radius: 6px; overflow: hidden; background: var(--ink-3); display: flex; align-items: center; justify-content: center; }
|
||||
.plan-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.plan-thumb-glyph { font-family: var(--font-mono); font-size: 15px; color: var(--faint); }
|
||||
|
||||
@@ -600,16 +600,9 @@
|
||||
});
|
||||
}
|
||||
|
||||
// accept a media asset dragged from the bin → sets this clip's source
|
||||
clip.addEventListener("dragover", (e) => { if (e.dataTransfer.types.includes("application/funpack-media")) { e.preventDefault(); clip.classList.add("drop-target"); } });
|
||||
clip.addEventListener("dragleave", () => clip.classList.remove("drop-target"));
|
||||
clip.addEventListener("drop", (e) => {
|
||||
const id = e.dataTransfer.getData("application/funpack-media");
|
||||
clip.classList.remove("drop-target");
|
||||
// Dropping on a clip sets ITS anchor; stop the event reaching the track-level
|
||||
// drop (which would otherwise also create a brand-new clip).
|
||||
if (id) { e.preventDefault(); e.stopPropagation(); S.assignMediaToScene(scene.id, id); }
|
||||
});
|
||||
// i2v anchors are set on the PLAN, not here — the timeline is the rendered result.
|
||||
// (Drop an image onto a Plan card to anchor that scene.) Video clips still drop on the
|
||||
// timeline track below.
|
||||
|
||||
// i2v anchor thumbnail (image + mixed + generated_frame)
|
||||
const mref = anchorMediaRef(scene, p);
|
||||
@@ -1878,14 +1871,10 @@
|
||||
const asset = (st.mediaBin || []).find((m) => m.id === id);
|
||||
if (asset?.kind === "video") {
|
||||
e.preventDefault();
|
||||
S.addVideoClip(id);
|
||||
} else if (asset?.kind === "image") {
|
||||
// CapCut-style: drop an image on empty timeline → default-length generative
|
||||
// scene anchored to it. (Dropping ON a clip sets that clip's anchor instead;
|
||||
// that handler stops propagation so this one doesn't also fire.)
|
||||
e.preventDefault();
|
||||
S.addImageClip(id);
|
||||
S.addVideoClip(id); // a finished video is a result — it belongs on the timeline
|
||||
}
|
||||
// Images are i2v ANCHORS — drop them on a Plan card (or the Plan strip) instead, so
|
||||
// the timeline (the rendered result) is never changed by setting up a generation.
|
||||
});
|
||||
lay.forEach(({ seg, o, d }) => {
|
||||
if (seg.kind === "gap") return;
|
||||
|
||||
Reference in New Issue
Block a user