From b18ac237fdf1c5fe1215d85f8da2dbb8308ca0d1 Mon Sep 17 00:00:00 2001 From: bbc-s Date: Wed, 10 Jun 2026 18:32:37 +0200 Subject: [PATCH] Improve editor UI parity and centering --- web/js/zimage_turbo_region_builder.js | 86 +++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 10 deletions(-) diff --git a/web/js/zimage_turbo_region_builder.js b/web/js/zimage_turbo_region_builder.js index f1dfa37..3aa4841 100644 --- a/web/js/zimage_turbo_region_builder.js +++ b/web/js/zimage_turbo_region_builder.js @@ -71,16 +71,18 @@ function injectStyle() { const s = document.createElement("style"); s.id = "kjideo-style"; s.textContent = ` - .kjideo-wrap { display:flex; flex-direction:column; overflow:hidden; position:relative; pointer-events:auto; gap:4px; min-width:0; } + .kjideo-wrap { display:flex; flex-direction:column; overflow:hidden; position:relative; pointer-events:auto; gap:4px; min-width:0; align-items:stretch; } .kjideo-canvas { cursor:crosshair; display:block; max-width:100%; width:100%; height:auto; flex:0 0 auto; align-self:center; background:#1a1a1a; border-radius:4px; outline:none; object-fit:contain; } .kjideo-bar { display:flex; align-items:center; gap:6px; flex-wrap:wrap; font:11px sans-serif; color:#aaa; user-select:none; padding:0 2px; flex:0 0 auto; min-width:0; } .kjideo-bar span { min-width:0; overflow:hidden; text-overflow:ellipsis; } + .kjideo-actions { display:flex; align-items:center; justify-content:flex-end; gap:6px; flex-wrap:wrap; flex:0 1 auto; min-width:148px; margin-left:auto; } .kjideo-panel { display:flex; flex-direction:column; gap:5px; padding:6px; background:#262626; border-radius:4px; font:11px sans-serif; color:#bbb; flex:0 0 auto; min-width:0; } .kjideo-row { display:flex; align-items:center; gap:6px; flex-wrap:wrap; } .kjideo-btn { background:#333; border:1px solid #555; border-radius:4px; color:#bbb; font:11px sans-serif; cursor:pointer; padding:2px 8px; line-height:16px; white-space:nowrap; flex-shrink:0; } .kjideo-btn:hover { border-color:#46b4e6; color:#fff; } .kjideo-btn.active { border-color:#46b4e6; color:#46b4e6; background:#2a3a42; } - .kjideo-area { width:100%; box-sizing:border-box; background:#1d1d1d; border:1px solid #444; border-radius:4px; color:#ddd; font:13px monospace; padding:4px 6px; resize:none; min-height:36px; max-height:96px; overflow:auto; } + .kjideo-label { color:#aaa; font:11px sans-serif; line-height:13px; } + .kjideo-area { width:100%; box-sizing:border-box; background:#1d1d1d; border:1px solid #444; border-radius:4px; color:#ddd; font:13px monospace; padding:4px 6px; resize:vertical; min-height:36px; max-height:160px; overflow:auto; } .kjideo-sw { width:20px; height:20px; border:1px solid #666; border-radius:3px; cursor:pointer; flex-shrink:0; position:relative; transition:transform .18s ease, box-shadow .12s ease, opacity .12s ease; } .kjideo-sw:hover { transform:scale(1.2); box-shadow:0 0 0 2px #46b4e6; z-index:3; } .kjideo-sw.dragging { opacity:.4; box-shadow:0 0 0 2px #46b4e6; } @@ -118,10 +120,12 @@ app.registerExtension({ const stylePaletteWidget = null; const bgBrightnessWidget = findW("bg_brightness"); if (bgBrightnessWidget && typeof bgBrightnessWidget.value !== "number") bgBrightnessWidget.value = 25; + const globalPromptWidget = findW("global_prompt"); + const globalNegativeWidget = findW("global_negative_prompt"); const wWidget = findW("width"), hWidget = findW("height"); // Hide the data widgets while keeping them serializable. function hideDataWidgets() { - for (const w of [elementsWidget, bgBrightnessWidget]) { + for (const w of [elementsWidget, bgBrightnessWidget, globalPromptWidget, globalNegativeWidget]) { if (!w) continue; w.hidden = true; w.computeSize = () => [0, -4]; @@ -150,6 +154,8 @@ app.registerExtension({ node._lastImported = ""; // last import_json applied to the editor (avoid re-apply) node._areaH = node._areaH || {}; node._areaObservers = []; + node._globalAreaH = node._globalAreaH || {}; + node._globalAreaObservers = []; // ── DOM ── const wrap = document.createElement("div"); @@ -202,7 +208,10 @@ app.registerExtension({ bgSlider.style.cssText = "width:64px;flex:0 0 auto;"; stopProp(bgSlider); bgSlider.addEventListener("input", () => { if (bgBrightnessWidget) bgBrightnessWidget.value = parseInt(bgSlider.value, 10); drawCanvas(); }); - bar.appendChild(hint); bar.appendChild(liveLabel); bar.appendChild(grabBtn); bar.appendChild(bgSlider); bar.appendChild(tokenSpan); bar.appendChild(copyBtn); bar.appendChild(importBtn); bar.appendChild(clearBtn); + const actions = document.createElement("div"); + actions.className = "kjideo-actions"; + actions.append(liveLabel, grabBtn, bgSlider, tokenSpan, copyBtn, importBtn, clearBtn); + bar.append(hint, actions); updateGrabBtn(); // Persistent global style-palette row @@ -222,11 +231,14 @@ app.registerExtension({ addWheelPassthrough(wrap); addMiddleClickPan(canvasEl); + const globalPanel = document.createElement("div"); + globalPanel.className = "kjideo-panel"; + const panel = document.createElement("div"); panel.className = "kjideo-panel"; // Canvas above panel so the panel grows downward without shifting the canvas. - wrap.appendChild(bar); wrap.appendChild(styleBar); wrap.appendChild(canvasEl); wrap.appendChild(panel); + wrap.appendChild(bar); wrap.appendChild(styleBar); wrap.appendChild(globalPanel); wrap.appendChild(canvasEl); wrap.appendChild(panel); const TOOLBAR_H = 44; const MIN_EDITOR_H = 420; @@ -259,6 +271,7 @@ app.registerExtension({ const widgetH = Math.min(MAX_EDITOR_H, Math.max(MIN_EDITOR_H, node.size?.[1] || DEFAULT_EDITOR_H)); const reservedH = (bar.offsetHeight || TOOLBAR_H) + (styleBar.offsetHeight || 0) + + (globalPanel.offsetHeight || 96) + (panel.offsetHeight || 132) + 30; const maxCanvasW = Math.max(120, nodeW - 30); @@ -271,6 +284,8 @@ app.registerExtension({ } canvasEl.style.width = `${Math.round(displayW)}px`; canvasEl.style.height = `${Math.round(displayH)}px`; + canvasEl.style.marginLeft = "auto"; + canvasEl.style.marginRight = "auto"; } function recalcWidgetHeight() { @@ -434,6 +449,18 @@ app.registerExtension({ _draw(); }); } + function imageSize(img) { + return { + w: img?.naturalWidth || img?.videoWidth || img?.width || 1, + h: img?.naturalHeight || img?.videoHeight || img?.height || 1, + }; + } + function drawImageContain(img, x, y, w, h) { + const s = imageSize(img); + const scale = Math.min(w / s.w, h / s.h); + const dw = s.w * scale, dh = s.h * scale; + ctx.drawImage(img, x + (w - dw) / 2, y + (h - dh) / 2, dw, dh); + } function _draw() { // Size the backing store to display Ă— DPR and draw in logical px (crisp text/lines). const W = logW(), H = logH(), d = window.devicePixelRatio || 1; @@ -444,7 +471,8 @@ app.registerExtension({ let bri = bgBrightnessWidget ? bgBrightnessWidget.value : 25; if (typeof bri !== "number" || isNaN(bri)) bri = 25; // guard against unset widget value if (node._bgImg) { // reference image, dimmed by brightness - ctx.drawImage(node._bgImg, 0, 0, W, H); + ctx.fillStyle = "#1a1a1a"; ctx.fillRect(0, 0, W, H); + drawImageContain(node._bgImg, 0, 0, W, H); const dim = 1 - bri / 100; if (dim > 0) { ctx.fillStyle = `rgba(0,0,0,${dim})`; ctx.fillRect(0, 0, W, H); } } else { // blank canvas grey from brightness @@ -899,6 +927,12 @@ app.registerExtension({ const w = findW(name); return w ? w.value : ""; } + function setW(name, value) { + const w = findW(name); + if (!w) return; + w.value = value || ""; + w.callback?.(w.value); + } function buildCaption() { return JSON.stringify(buildZImageRegionPayload(), null, 2); } function updateTokens() { const txt = (getW("global_prompt") || "") + " " + node._boxes.map((b) => b.prompt || b.desc || "").join(" "); @@ -917,6 +951,8 @@ app.registerExtension({ } function loadCaption(cap) { closeInlineEditor(); + if (typeof cap.global_prompt === "string") setW("global_prompt", cap.global_prompt); + if (typeof cap.global_negative_prompt === "string") setW("global_negative_prompt", cap.global_negative_prompt); const W = wWidget?.value || 1024, H = hWidget?.value || 1024; node._boxes = (cap.regions || []).map((b, i) => { const bb = b.bbox && typeof b.bbox === "object" ? b.bbox : null; @@ -934,7 +970,7 @@ app.registerExtension({ }; }); node._activeIdx = node._boxes.length ? 0 : -1; - syncCanvasToDims(); commit(); fitNode(); + syncCanvasToDims(); renderGlobalPanel(); commit(); fitNode(); } async function doCopy() { const txt = buildCaption(); @@ -1064,17 +1100,44 @@ app.registerExtension({ } // Textarea whose user-dragged height persists across panel rebuilds / box switches. - function makeArea(field, value, placeholder, onInput, defaultH) { + function makeArea(field, value, placeholder, onInput, defaultH, heightStore = node._areaH, observers = node._areaObservers) { const ta = document.createElement("textarea"); ta.className = "kjideo-area"; ta.placeholder = placeholder; ta.value = value || ""; - const h = defaultH; + const h = heightStore[field] || defaultH; if (h) ta.style.height = h + "px"; stopProp(ta); ta.addEventListener("input", onInput); + if (window.ResizeObserver) { + const ro = new ResizeObserver(() => { + const px = Math.round(ta.getBoundingClientRect().height); + if (px > 0) heightStore[field] = px; + scheduleFit(); + }); + ro.observe(ta); + observers.push(ro); + } return ta; } + function addAreaLabel(container, text) { + const label = document.createElement("div"); + label.className = "kjideo-label"; + label.textContent = text; + container.appendChild(label); + } + function renderGlobalPanel() { + for (const ro of node._globalAreaObservers) ro.disconnect(); + node._globalAreaObservers = []; + globalPanel.innerHTML = ""; + addAreaLabel(globalPanel, "positive prompt"); + globalPanel.appendChild(makeArea("global_prompt", getW("global_prompt"), "global positive prompt", + function () { setW("global_prompt", this.value); updateTokens(); drawCanvas(); }, 64, node._globalAreaH, node._globalAreaObservers)); + addAreaLabel(globalPanel, "negative prompt"); + globalPanel.appendChild(makeArea("global_negative_prompt", getW("global_negative_prompt"), "global negative prompt", + function () { setW("global_negative_prompt", this.value); updateTokens(); }, 48, node._globalAreaH, node._globalAreaObservers)); + scheduleFit(); + } function renderPanel() { for (const ro of node._areaObservers) ro.disconnect(); node._areaObservers = []; @@ -1092,8 +1155,10 @@ app.registerExtension({ const col = REGION_COLORS[node._activeIdx % REGION_COLORS.length]; hint.innerHTML = `region ${node._activeIdx + 1} · dbl-click edit · alt-click overlap · del remove`; + addAreaLabel(panel, "region prompt"); panel.appendChild(makeArea("prompt", b.prompt || b.desc || "", "region prompt", function () { b.prompt = this.value; b.desc = this.value; touch(); }, 92)); + addAreaLabel(panel, "region negative prompt"); panel.appendChild(makeArea("negative_prompt", b.negative_prompt || "", "optional region negative prompt", function () { b.negative_prompt = this.value; touch(); }, 56)); @@ -1130,7 +1195,7 @@ app.registerExtension({ // Update the token estimate when the caption-level text widgets change. for (const name of ["global_prompt", "global_negative_prompt"]) { const w = findW(name); - if (w) chainCallback(w, "callback", () => updateTokens()); + if (w) chainCallback(w, "callback", () => { updateTokens(); }); } // ── keep canvas + getMinHeight in sync while the node is resized ── @@ -1236,6 +1301,7 @@ app.registerExtension({ if (bgBrightnessWidget) bgSlider.value = bgBrightnessWidget.value; syncCanvasToDims(); rebuildStylePalette(); + renderGlobalPanel(); renderPanel(); drawCanvas(); updateTokens();