import { api } from "/scripts/api.js"; import { app } from "/scripts/app.js"; import { hideInputsInDef, hideWidget } from "./widget_visibility.mjs"; import { mountTransformTrim } from "./transform_trim.mjs"; import { clipOutputRate, inputNumber } from "./clip_rate.mjs"; import { BRAND, chainCallback, keepDomWidgetWidthAuto, notifyAusbossChange, showToast } from "./index.mjs"; import { fillNodeHeight } from "./panel_layout.mjs"; import { mediaViewQuery } from "./media_list.mjs"; import { createMediaPicker } from "./media_picker.mjs"; import { normalizeFillColor } from "./fill_color.mjs"; import { makeScrubInput } from "./scrub_input.mjs"; import { featherGeneratedMask, overlayPlan, stitchBlendFromMask } from "./stitch_preview.mjs"; import { INPUT_FOLDER_MODE, LOCAL_PATH_MODE, normalizeVideoOptions, mediaSourceState, videoSourceState, } from "./video_source_card.mjs"; import { canvasLocalPoint, clamp, declaredTransformDefaults, fitSourceToAspect, cropHandleCenters, lockPadding, lockedPadMinimum, nearestHandle, paddingAxis, paddingHandleCenters, parseAspectRatio, resetTransformValues, resizeCrop, resolveCrop, resolvePadding, rotatedSize, scaleToMegapixels, sourceChanged, sourceResetValues, stageHandleLayout, zoomAround, } from "./transform_geometry.mjs"; import { clampFrame, clipInfo, frameTime, frameWindow, windowSeconds } from "./timeline_math.mjs"; import { liftSocket } from "./widget_card.mjs"; const HIDDEN_WIDGETS = [ "image", "upload", "video", "source_mode", "local_path", "rotation_degrees", "crop_aspect_ratio", "crop_x", "crop_y", "crop_width", "crop_height", "pad_left", "pad_top", "pad_right", "pad_bottom", "feather", "canvas_multiple", "fill_color", "seek_mode", "frame_index", "frame_time", "start_seconds", "end_seconds", "every_nth", "max_frames", "frame_snap", "fixed_frames", // Clip-node stitch settings, driven by the editor's Inpaint & Stitch section. "stitch_blend", "stitch_grow", // Image-node resize block; hideWidget on a missing widget is a no-op, so // the video node sharing this list is unaffected. "resize_to_megapixels", "megapixels", "resize_method", "resolution_steps", ]; const trimInputDriven = (node, name) => node.inputs?.some( (input) => (input.name === name || input.widget?.name === name) && input.link != null, ) ?? false; const RESIZE_METHODS = ["lanczos", "area", "bicubic", "bilinear", "nearest-exact"]; const TRANSFORM_DEFAULTS = resetTransformValues(false); const CORE_IMAGE_PREVIEW_WIDGET = "$$canvas-image-preview"; function installStyles() { if (document.getElementById("ausboss-transform-styles")) return; const style = document.createElement("style"); style.id = "ausboss-transform-styles"; style.textContent = ` .ausboss-transform-panel{display:flex;flex-direction:column;gap:8px;padding:8px;color:#ddd;font:12px system-ui;box-sizing:border-box;width:100%;height:100%;overflow:hidden} .ausboss-transform-preview{width:100%;flex:1 1 180px;min-height:0;border:1px solid #50555b;border-radius:8px;background:#111;display:block;touch-action:none} .ausboss-transform-source{display:flex;flex-direction:column;gap:7px;flex:0 0 auto;padding:8px;border:1px solid rgba(0,184,174,.28);border-radius:8px;background:rgba(0,0,0,.24)} .ausboss-transform-source-heading{color:${BRAND};font:600 10px system-ui;letter-spacing:.08em;text-transform:uppercase} .ausboss-transform-source-mode{display:grid;grid-template-columns:1fr 1fr;height:30px;padding:3px;border:1px solid #2a3437;border-radius:7px;background:#0f1516} .ausboss-transform-source-mode button{border:0;border-radius:5px;background:transparent;color:#8ba3a1;font:600 12px system-ui;cursor:pointer} .ausboss-transform-source-mode button:hover{color:#fff}.ausboss-transform-source-mode button.on{background:${BRAND};color:#04201d} .ausboss-transform-source-field{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px;align-items:center} .ausboss-transform-source-field select,.ausboss-transform-source-field input{box-sizing:border-box;width:100%;height:34px;min-width:0;padding:0 10px;border:1px solid #2a3437;border-radius:7px;outline:0;background:#0b0f10;color:#dce9e8;font:12px ui-monospace,"SF Mono",Menlo,Consolas,monospace} .ausboss-transform-source-field select:focus,.ausboss-transform-source-field input:focus{border-color:${BRAND}} .ausboss-transform-source-field .ausboss-media-pick{box-sizing:border-box;width:100%;height:34px;min-width:0;padding:0 28px 0 10px;border:1px solid #2a3437;border-radius:7px;background:#0b0f10 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238ba3a1' stroke-width='1.5'/%3E%3C/svg%3E") no-repeat right 10px center;color:#dce9e8;font:12px ui-monospace,"SF Mono",Menlo,Consolas,monospace} .ausboss-transform-source-field .ausboss-media-pick:hover,.ausboss-transform-source-field .ausboss-media-pick:focus-visible{border-color:${BRAND};outline:0} .ausboss-transform-source-action{height:34px;box-sizing:border-box;white-space:nowrap} .ausboss-transform-source-field:has(input[type=text]){grid-template-columns:minmax(0,1fr)} .ausboss-transform-source-hint{overflow:hidden;color:#6f8886;font-size:10.5px;line-height:1.25;white-space:nowrap;text-overflow:ellipsis} .lg-node:has(.ausboss-transform-panel) .image-preview{display:none!important} .ausboss-transform-row{display:flex;gap:7px;align-items:center;flex:0 0 auto}.ausboss-transform-row>*{min-width:0;flex:1} .ausboss-transform-canvas-row{justify-content:space-between} .ausboss-transform-canvas-row>label{flex:0 0 auto;display:flex;align-items:center;gap:6px;color:#aeb4ba;font-size:11px;white-space:nowrap;cursor:default;user-select:none} .ausboss-transform-canvas-row>label>span{color:#8ca8a5;font-weight:600;font-size:10px;letter-spacing:.06em;text-transform:uppercase} .ausboss-transform-swatch{width:30px;height:22px;padding:1px;border:1px solid #555b63;border-radius:5px;background:#23272c;cursor:pointer} .ausboss-transform-swatch::-webkit-color-swatch-wrapper{padding:1px}.ausboss-transform-swatch::-webkit-color-swatch{border:0;border-radius:3px} .ausboss-transform-canvas-row input[type=checkbox]{accent-color:${BRAND};margin:0;cursor:pointer} .ausboss-transform-aspects{display:flex;gap:5px;align-items:center;flex:0 0 auto} .ausboss-transform-aspects>span{flex:0 0 auto;color:#8ca8a5;font-size:10px;padding:0 3px;user-select:none} .ausboss-transform-aspect{flex:1 1 0;min-width:0;background:#262a30;color:#cfd6dc;border:1px solid #4a5058;border-radius:4px;height:28px;padding:3px 2px;font:600 10px system-ui;font-variant-numeric:tabular-nums;white-space:nowrap;cursor:pointer;text-align:center} .ausboss-transform-aspect-flip{flex:0 0 30px;display:flex;align-items:center;justify-content:center;margin-right:3px} .ausboss-transform-aspect-glyph{display:block;border:1px solid currentColor;border-radius:1px;box-sizing:border-box} .ausboss-transform-aspect:hover{border-color:${BRAND};color:#fff} .ausboss-transform-aspect.active{background:rgba(0,184,174,.18);border-color:${BRAND};color:#e5fffc} .ausboss-transform-aspect.locked{background:rgba(0,184,174,.34);border-color:#e5fffc;color:#fff} .ausboss-transform-aspect-lock{display:inline-block;margin-left:3px;vertical-align:-1px;line-height:0} .ausboss-transform-button,.ausboss-transform-modal button{background:#30343a;color:#eee;border:1px solid #555b63;border-radius:5px;padding:7px 10px;cursor:pointer} .ausboss-transform-button:hover,.ausboss-transform-modal button:hover{border-color:${BRAND};background:#383e44} .ausboss-transform-file{position:relative;text-align:center;overflow:hidden}.ausboss-transform-file input{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer} .ausboss-transform-modal{position:fixed;inset:0;z-index:100000;background:#101214;color:#e6e8ea;font:13px system-ui;display:grid;grid-template-rows:42px minmax(0,1fr) auto} .ausboss-transform-header{display:flex;align-items:center;gap:12px;padding:0 12px;border-bottom:1px solid #30343a;background:#17191c} .ausboss-transform-header strong{color:#fff}.ausboss-transform-header .spacer{flex:1} .ausboss-transform-close{background:${BRAND}!important;border-color:${BRAND}!important;color:#06231f!important;font-weight:600} .ausboss-transform-close:hover{filter:brightness(1.15)} .ausboss-transform-danger{background:#4a1717!important;border-color:#a13a3a!important;color:#ffd9d9!important} .ausboss-transform-danger:hover{background:#6b1f1f!important;border-color:#c74e4e!important} .ausboss-transform-body{display:grid;grid-template-columns:270px minmax(320px,1fr) 250px;min-height:0} .ausboss-transform-sidebar{padding:12px;border-right:1px solid #30343a;overflow:auto;background:#181b1e} .ausboss-transform-sidebar.right{border-right:0;border-left:1px solid #30343a} .ausboss-transform-section{border-bottom:1px solid #34383d;padding:0 0 13px;margin:0 0 13px} .ausboss-transform-section h3{font-size:11px;color:${BRAND};text-transform:uppercase;margin:0 0 8px;display:flex;align-items:center;gap:7px} .ausboss-legend{display:inline-block;flex:0 0 auto;width:10px;height:10px} canvas.ausboss-legend{width:16px;height:16px} .ausboss-legend-crop{background:#4bd8ef;border:1px solid #08272d} .ausboss-legend-pad{background:#ff9d42;border:1px solid #3b2108;width:9px;height:9px;transform:rotate(45deg)} .ausboss-final-preview{display:block;max-width:100%;margin:2px auto 0;border:1px solid #34383d;border-radius:6px;background:#0c0e10} .ausboss-transform-section label{display:grid;grid-template-columns:88px 1fr 58px;gap:7px;align-items:center;margin:7px 0} .ausboss-transform-section label>.ausboss-scrub:last-child{width:58px} .ausboss-transform-section label>*{min-width:0} .ausboss-transform-section input,.ausboss-transform-section select{box-sizing:border-box;width:100%;background:#0e1012;color:#eee;border:1px solid #454b52;border-radius:4px;padding:5px} .ausboss-transform-stage{position:relative;min-width:0;min-height:0;background-color:#0c0e10;background-image:radial-gradient(#292d31 1px,transparent 1px);background-size:18px 18px;overflow:hidden} .ausboss-transform-canvas{width:100%;height:100%;display:block;touch-action:none} .ausboss-transform-status{line-height:1.55;color:#b8bec5;white-space:pre-wrap}.ausboss-transform-help{line-height:1.55;color:#aeb4ba} .ausboss-transform-sidebar.right .ausboss-transform-section{margin-top:13px} .ausboss-transform-section details{margin:8px 0 0}.ausboss-transform-section summary{cursor:pointer;color:#8de0da;font-size:11px;text-transform:uppercase;letter-spacing:.06em;list-style:none;user-select:none} .ausboss-transform-section summary::before{content:"▸";display:inline-block;width:12px;transition:transform .12s}.ausboss-transform-section details[open] summary::before{transform:rotate(90deg)} .ausboss-transform-section input[type=checkbox]{width:auto;justify-self:start;accent-color:${BRAND}} .ausboss-transform-timeline{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:8px 12px;border-top:1px solid #30343a;background:#17191c} .ausboss-transform-timeline>.ausboss-transform-trim{flex-basis:100%} .ausboss-transform-transport{display:flex;flex-wrap:wrap;align-items:center;gap:8px;flex-basis:100%} .ausboss-transform-steps{display:flex;gap:4px;flex-wrap:wrap}.ausboss-transform-steps button{padding:5px 7px} .ausboss-transform-badge{padding:3px 7px;border-radius:99px;background:#263034;color:#8de0da;font-size:11px} @media(max-width:900px){.ausboss-transform-body{grid-template-columns:220px minmax(260px,1fr)}.ausboss-transform-sidebar.right{display:none}} `; document.head.appendChild(style); } function widget(node, name) { return node.widgets?.find((item) => item.name === name); } function value(node, name, fallback = 0) { return widget(node, name)?.value ?? fallback; } function setValue(node, name, next) { const target = widget(node, name); if (!target) return; target.value = next; target.callback?.(next); } function values(node) { return Object.fromEntries(Object.keys(TRANSFORM_DEFAULTS).map((name) => [name, value(node, name, TRANSFORM_DEFAULTS[name])])); } function suppressCoreImagePreview(node) { const previewIndex = node.widgets?.findIndex((item) => item.name === CORE_IMAGE_PREVIEW_WIDGET) ?? -1; if (previewIndex >= 0) { node.widgets[previewIndex].onRemove?.(); node.widgets.splice(previewIndex, 1); } if (node.__ausbossImgsSuppressed) return; node.__ausbossImgsSuppressed = true; node.__ausbossAddCustomWidget = node.addCustomWidget; if (typeof node.addCustomWidget === "function") { node.addCustomWidget = function (customWidget) { if (customWidget?.name === CORE_IMAGE_PREVIEW_WIDGET) hideWidget(customWidget); return node.__ausbossAddCustomWidget.call(this, customWidget); }; } node.__ausbossImgsDescriptor = Object.getOwnPropertyDescriptor(node, "imgs"); Object.defineProperty(node, "imgs", { configurable: true, enumerable: true, get() { return undefined; }, set() {}, }); } function sourceKey(node, kind) { if (kind === "image") return String(value(node, "image", "")); return videoSourceState( value(node, "source_mode", INPUT_FOLDER_MODE), value(node, "video", ""), value(node, "local_path", ""), ).key; } function parseInputReference(selection) { const normalized = String(selection || "").replaceAll("\\", "/"); const parts = normalized.split("/"); const filename = parts.pop() || ""; return { filename, subfolder: parts.join("/"), type: "input" }; } function imageSourceUrl(selection) { const reference = parseInputReference(selection); return api.apiURL(`/view?${new URLSearchParams(reference)}`); } function videoParams(node, maxSize = 1600) { return new URLSearchParams({ source_mode: String(value(node, "source_mode", "input folder")), video: String(value(node, "video", "")), local_path: String(value(node, "local_path", "")), seek_mode: String(value(node, "seek_mode", "frame index")), frame_index: String(Math.max(0, Math.round(Number(value(node, "frame_index", 0)) || 0))), frame_time: String(Math.max(0, Number(value(node, "frame_time", 0)) || 0)), max_width: String(maxSize), max_height: String(maxSize), }); } async function uploadMedia(node, kind, file) { const body = new FormData(); body.append("image", file, file.name); body.append("type", "input"); const route = kind === "video" ? "/ausboss/transform/video/upload" : "/upload/image"; const response = await api.fetchApi(route, { method: "POST", body }); if (!response.ok) { const text = await response.text(); let message = text; try { message = JSON.parse(text).error || text; } catch {} throw new Error(message || "Upload failed."); } const result = await response.json(); const selection = result.subfolder ? `${result.subfolder}/${result.name}` : result.name; const target = widget(node, kind); if (Array.isArray(target?.options?.values) && !target.options.values.includes(selection)) target.options.values.push(selection); if (kind === "video") setValue(node, "source_mode", "input folder"); setValue(node, kind, selection); return selection; } function resetTransform(node, includeTimeline = false) { if (node.properties) { delete node.properties.ausboss_fit_aspect; delete node.properties.ausboss_aspect_lock; } for (const [name, next] of Object.entries(declaredTransformDefaults(node.constructor?.nodeData, includeTimeline))) setValue(node, name, next); node.setDirtyCanvas?.(true, true); } // The node face's Reset: only the shape - rotation, crop, padding. Fill, // feather and Align stay, as they do when the source changes. function resetGeometry(node) { if (node.properties) { delete node.properties.ausboss_fit_aspect; delete node.properties.ausboss_aspect_lock; } for (const [name, next] of Object.entries(sourceResetValues(false))) setValue(node, name, next); node.setDirtyCanvas?.(true, true); } function createElement(tag, className, text) { const element = document.createElement(tag); if (className) element.className = className; if (text !== undefined) element.textContent = text; return element; } function addLabeledControl(section, title, control, suffix = "") { const label = createElement("label"); label.append(createElement("span", "", title), control, createElement("span", "", suffix)); section.append(label); return label; } function buildMediaSourceCard(state) { const { node, kind } = state; const root = createElement("div", "ausboss-transform-source"); const modes = createElement("div", "ausboss-transform-source-mode"); const uploadsMode = createElement("button", "", "Uploads"); const localMode = createElement("button", "", "Local path"); uploadsMode.type = localMode.type = "button"; uploadsMode.title = "Choose a video already in ComfyUI's input folder or upload another."; localMode.title = "Read a video inside ComfyUI's input, output or temp folder in place, without copying it."; modes.append(uploadsMode, localMode); const field = createElement("div", "ausboss-transform-source-field"); // The media picker previews the hovered file, which a native