fix: overlay colours accept #rgb/#rrggbbaa, orphan overlays show, dialog keeps project-width default and dead pictures, a failing lane feature cannot stop the timeline
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
edbd8dc1c3
commit
dd600040a5
@@ -27,7 +27,7 @@ function textForm(s) {
|
||||
|
||||
function imageForm(s, bin) {
|
||||
return [
|
||||
field("Picture", c.select.md({ label: "Picture", value: s.media_ref || "", options: [{ value: "", label: "Choose a picture…" }, ...bin.map((m) => ({ value: m.id, label: m.name }))],
|
||||
field("Picture", c.select.md({ label: "Picture", value: s.media_ref || "", options: [{ value: "", label: "Choose a picture…" }, ...(s.media_ref && !bin.some((m) => m.id === s.media_ref) ? [{ value: s.media_ref, label: "(no longer in the media bin)" }] : []), ...bin.map((m) => ({ value: m.id, label: m.name }))],
|
||||
onChange: (v) => { s.media_ref = v; s.label = (bin.find((m) => m.id === v) || {}).name || "Image"; } }), bin.length ? undefined : "The media bin has no pictures yet."),
|
||||
field("Width (px)", c.number.md({ label: "Width", value: s.width_px, min: 8, step: 1, precision: 0, onChange: (v) => { s.width_px = v; } })),
|
||||
c.toggle.default({ label: "Keep the picture's proportions", checked: s.keep_aspect !== false, onChange: (v) => { s.keep_aspect = v; if (!v && s.height_px == null) s.height_px = s.width_px; } }),
|
||||
@@ -40,13 +40,14 @@ function imageForm(s, bin) {
|
||||
|
||||
/** kind "text"|"image", `existing` an overlay (edit) or null (new), `bin` the pictures to choose from.
|
||||
* `onSave(patch)` runs once with the chosen values; the window closes after it. */
|
||||
export function openDialog({ kind, existing, bin = [], onSave }) {
|
||||
const s = kind === "text" ? { ...TEXT_DEFAULTS, text: "Title", ...(existing || {}) } : { width_px: 270, keep_aspect: true, opacity: 1, ...(existing || {}) };
|
||||
export function openDialog({ kind, existing, bin = [], projectWidth = 768, onSave }) {
|
||||
const s = kind === "text" ? { ...TEXT_DEFAULTS, text: "Title", ...(existing || {}) } : { width_px: Math.round(projectWidth * 0.35), keep_aspect: true, opacity: 1, ...(existing || {}) };
|
||||
const where = field("Starts at (s)", c.number.md({ label: "Starts at", value: existing ? existing.start_sec : undefined, min: 0, step: 0.1, onChange: (v) => { s.start_sec = v; } }));
|
||||
const dur = field("Shown for (s)", c.number.md({ label: "Shown for", value: s.duration_sec ?? 3, min: 0.1, step: 0.1, onChange: (v) => { s.duration_sec = v; } }));
|
||||
const body = c.region.stack({ gap: "sm", children: [...(kind === "text" ? textForm(s) : imageForm(s, bin)), ...(existing ? [where, dur] : [])] });
|
||||
const save = () => {
|
||||
if (kind === "image" && !s.media_ref) return c.toast.warn({ text: "Choose a picture first." });
|
||||
if (kind === "text") s.text = String(s.text || "").trim() || "Title"; // what the render would draw anyway
|
||||
onSave(s);
|
||||
win.close("done");
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { learn } from "../../shell/bin.js";
|
||||
import { openDialog } from "./dialog.js";
|
||||
|
||||
const ID = "o:";
|
||||
const short = (o) => (o.kind === "text" ? o.text : o.label || "Image").split("\n")[0].slice(0, 40);
|
||||
const short = (o) => String((o.kind === "text" ? o.text : o.label) || "Overlay").split("\n")[0].slice(0, 40);
|
||||
|
||||
export default {
|
||||
id: "overlays",
|
||||
@@ -19,7 +19,7 @@ export default {
|
||||
async function open(kind, existing) {
|
||||
let bin = [];
|
||||
if (kind === "image") { try { bin = await pictures(); } catch (err) { return c.toast.warn({ text: `Could not read the media bin: ${err.message}` }); } }
|
||||
openDialog({ kind, existing, bin, onSave: (s) => edit((pr) => {
|
||||
openDialog({ kind, existing, bin, projectWidth: p.project ? p.project.width : undefined, onSave: (s) => edit((pr) => {
|
||||
if (existing) return ov.update(pr, existing.id, s);
|
||||
const at = app.playhead.at, made = kind === "text" ? ov.addText(pr, at, s) : ov.addImage(pr, s.media_ref, s.label, at);
|
||||
return ov.update(pr, made.id, s);
|
||||
@@ -30,9 +30,10 @@ export default {
|
||||
const entry = {
|
||||
owns: (id) => id.startsWith(ID),
|
||||
lanes(pr) {
|
||||
const lanes = ov.lanesOf(pr);
|
||||
const known = new Set(ov.lanesOf(pr).map((l) => l.id));
|
||||
const lanes = [...(ov.overlaysOf(pr).some((o) => !known.has(o.lane_id)) ? [{ id: "", label: "Unplaced" }] : []), ...ov.lanesOf(pr)]; // a v4 project's overlays without a lane still show, at the bottom as they render
|
||||
return [...lanes].reverse().map((lane) => ({ id: `ov:${lane.id}`, label: lane.label, kind: "overlay", move: true,
|
||||
clips: ov.overlaysOf(pr).filter((o) => o.lane_id === lane.id).map((o) => ({ id: ID + o.id, start: o.start_sec || 0, dur: o.duration_sec || 0.1, trim: true, title: short(o),
|
||||
clips: ov.overlaysOf(pr).filter((o) => (lane.id ? o.lane_id === lane.id : !known.has(o.lane_id))).map((o) => ({ id: ID + o.id, start: o.start_sec || 0, dur: o.duration_sec || 0.1, trim: true, title: short(o),
|
||||
head: [o.kind === "text" ? "T" : "▣"],
|
||||
actions: [{ label: "✎", title: "Edit", onClick: () => open(o.kind, o) }, { label: "▲", title: "Draw above the others (higher lane)", onClick: () => edit((x) => ov.restack(x, o.id, 1)) },
|
||||
{ label: "▼", title: "Draw below the others (lower lane)", onClick: () => edit((x) => ov.restack(x, o.id, -1)) }, { label: "✕", title: "Remove", onClick: () => confirmRemove(o) }] })) }));
|
||||
|
||||
@@ -44,7 +44,7 @@ const stage = {
|
||||
const ghostActions = (g) => [{ label: "✕", title: "Remove from the timeline",
|
||||
onClick: () => edit((pr) => { pr.scene_ghosts = (pr.scene_ghosts || []).filter((x) => x.id !== g.id); return true; }) }];
|
||||
|
||||
const draw = () => { const open = p.project; view.setLanes(open ? [videoLane(open, sel.ids, sel.focus, clipActions, ghostActions), audioLane(open, sel.ids), tracksLane(open), ...extra.flatMap((e) => e.lanes(open))].filter(Boolean) : []); };
|
||||
const draw = () => { const open = p.project; view.setLanes(open ? [videoLane(open, sel.ids, sel.focus, clipActions, ghostActions), audioLane(open, sel.ids), tracksLane(open), ...extra.flatMap((e) => { try { return e.lanes(open); } catch { return []; } })] // a feature's lanes failing must not stop the baseline's.filter(Boolean) : []); };
|
||||
const setZoom = (z) => { zoom = Math.min(ZOOM.length - 1, Math.max(0, z)); view.setZoom(ZOOM[zoom]); };
|
||||
host.append(view.node);
|
||||
const off = [app.on((what) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ export function addLane(p) {
|
||||
return lane;
|
||||
}
|
||||
|
||||
/** Drop a lane and everything on it. The last lane stays only while it holds something. */
|
||||
/** Drop a lane and everything on it. */
|
||||
export function removeLane(p, laneId) {
|
||||
if (!lanesOf(p).some((l) => l.id === laneId)) return false;
|
||||
p.overlay_lanes = lanesOf(p).filter((l) => l.id !== laneId);
|
||||
|
||||
@@ -59,11 +59,20 @@ def _fontfile(family) -> str | None:
|
||||
|
||||
def _rgba(color, opacity) -> tuple[int, int, int, int]:
|
||||
c = str(color or "#ffffff").strip().lstrip("#")
|
||||
if len(c) in (3, 4): # #rgb / #rgba
|
||||
c = "".join(ch * 2 for ch in c)
|
||||
alpha = 1.0
|
||||
if len(c) == 8: # #rrggbbaa: its alpha scales the overlay's opacity
|
||||
try:
|
||||
alpha, c = int(c[6:], 16) / 255, c[:6]
|
||||
except ValueError:
|
||||
c = "ffffff"
|
||||
opacity = _num(opacity, 1.0, 0.0, 1.0) * alpha
|
||||
try:
|
||||
r, g, b = (int(c[i:i + 2], 16) for i in (0, 2, 4)) if len(c) == 6 else (255, 255, 255)
|
||||
except ValueError:
|
||||
r, g, b = 255, 255, 255
|
||||
return r, g, b, int(_num(opacity, 1.0, 0.0, 1.0) * 255)
|
||||
return r, g, b, int(opacity * 255)
|
||||
|
||||
|
||||
def _variant(regular: str, bold: bool, italic: bool) -> str | None:
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from modules.output.render.overlays import _rgba
|
||||
|
||||
|
||||
def test_short_and_alpha_hex_colours():
|
||||
assert _rgba("#f00", 1) == (255, 0, 0, 255)
|
||||
assert _rgba("#ff000080", 1) == (255, 0, 0, 128)
|
||||
assert _rgba("#00ff00", 0.5) == (0, 255, 0, 127)
|
||||
assert _rgba("nonsense", 1) == (255, 255, 255, 255)
|
||||
Reference in New Issue
Block a user