2026-05-18-19-56-47 重构双视频同步与单帧对比
This commit is contained in:
261
frontend/app.js
261
frontend/app.js
@@ -18,6 +18,9 @@ const videoPreview = document.querySelector("#videoPreview");
|
||||
const imagePreview = document.querySelector("#imagePreview");
|
||||
const progressWrap = document.querySelector("#progressWrap");
|
||||
const progressText = document.querySelector("#progressText");
|
||||
const progressBar = document.querySelector("#progressBar");
|
||||
const resultVideoPreview = document.querySelector("#resultVideoPreview");
|
||||
const resultVideoEmpty = document.querySelector("#resultVideoEmpty");
|
||||
const summaryStrip = document.querySelector("#summaryStrip");
|
||||
const summaryJob = document.querySelector("#summaryJob");
|
||||
const summaryFrames = document.querySelector("#summaryFrames");
|
||||
@@ -34,23 +37,67 @@ const closeSourceDialog = document.querySelector("#closeSourceDialog");
|
||||
const sourceTitle = document.querySelector("#sourceTitle");
|
||||
const sourceVideo = document.querySelector("#sourceVideo");
|
||||
const sourceImage = document.querySelector("#sourceImage");
|
||||
const openCompareButton = document.querySelector("#openCompareButton");
|
||||
const controlCompareButton = document.querySelector("#controlCompareButton");
|
||||
const compareDialog = document.querySelector("#compareDialog");
|
||||
const closeCompareDialog = document.querySelector("#closeCompareDialog");
|
||||
const compareGrid = document.querySelector("#compareGrid");
|
||||
const compareTitle = document.querySelector("#compareTitle");
|
||||
|
||||
const methodLabels = new Map();
|
||||
const methodDescriptions = new Map();
|
||||
let selectedFile = null;
|
||||
let currentObjectUrl = null;
|
||||
let lastFrames = [];
|
||||
let lastResult = null;
|
||||
let currentFrame = null;
|
||||
let syncLock = false;
|
||||
let progressTimers = [];
|
||||
|
||||
function setBusy(isBusy, text = "运行分割") {
|
||||
const button = form.querySelector(".primary");
|
||||
button.disabled = isBusy;
|
||||
button.querySelector("span").textContent = isBusy ? "分割中" : text;
|
||||
progressWrap.hidden = !isBusy;
|
||||
if (isBusy) {
|
||||
progressText.textContent = "正在上传、抽帧并执行导丝分割";
|
||||
progressWrap.hidden = false;
|
||||
setProgress(8, "正在上传、抽帧并执行导丝分割");
|
||||
}
|
||||
}
|
||||
|
||||
function setProgress(percent, text) {
|
||||
progressBar.style.width = `${Math.max(0, Math.min(100, percent))}%`;
|
||||
progressText.textContent = text;
|
||||
}
|
||||
|
||||
function scheduleProgress() {
|
||||
progressTimers.forEach((timer) => clearTimeout(timer));
|
||||
progressTimers = [
|
||||
setTimeout(() => setProgress(28, "正在读取视频并抽取关键帧"), 400),
|
||||
setTimeout(() => setProgress(58, "正在生成导丝掩膜与叠加视频"), 1400),
|
||||
setTimeout(() => setProgress(82, "正在整理结果帧与指标"), 2800),
|
||||
];
|
||||
}
|
||||
|
||||
function finishProgress() {
|
||||
progressTimers.forEach((timer) => clearTimeout(timer));
|
||||
progressTimers = [];
|
||||
setProgress(100, "分割完成");
|
||||
setTimeout(() => {
|
||||
progressWrap.hidden = true;
|
||||
progressBar.style.width = "0%";
|
||||
}, 650);
|
||||
}
|
||||
|
||||
function failProgress(message) {
|
||||
progressTimers.forEach((timer) => clearTimeout(timer));
|
||||
progressTimers = [];
|
||||
setProgress(100, message || "分割失败");
|
||||
setTimeout(() => {
|
||||
progressWrap.hidden = true;
|
||||
progressBar.style.width = "0%";
|
||||
}, 900);
|
||||
}
|
||||
|
||||
function setFile(file) {
|
||||
selectedFile = file;
|
||||
if (typeof DataTransfer !== "undefined") {
|
||||
@@ -60,6 +107,7 @@ function setFile(file) {
|
||||
}
|
||||
fileName.textContent = `${file.name} · ${(file.size / 1024 / 1024).toFixed(2)} MB`;
|
||||
renderPreview(file);
|
||||
resetResultsForNewInput();
|
||||
}
|
||||
|
||||
function revokePreview() {
|
||||
@@ -125,6 +173,7 @@ async function loadMethods() {
|
||||
methodSelect.innerHTML = "";
|
||||
methodGrid.innerHTML = "";
|
||||
Object.entries(data.methods).forEach(([key, value]) => {
|
||||
if (key === "compare") return;
|
||||
methodLabels.set(key, value.label);
|
||||
methodDescriptions.set(key, value.description);
|
||||
|
||||
@@ -154,7 +203,22 @@ function updateSummary(data) {
|
||||
summarySkeleton.textContent = Math.round(skeleton);
|
||||
summaryStrip.hidden = false;
|
||||
resultCount.textContent = `${frames.length} 个结果`;
|
||||
jobMeta.textContent = `${data.kind === "video" ? "视频" : "图像"} · ${methodLabels.get(data.method) || data.method}`;
|
||||
}
|
||||
|
||||
function resetResultsForNewInput() {
|
||||
lastFrames = [];
|
||||
lastResult = null;
|
||||
currentFrame = null;
|
||||
resultGrid.innerHTML = "";
|
||||
resultVideoPreview.hidden = true;
|
||||
resultVideoPreview.removeAttribute("src");
|
||||
resultVideoEmpty.hidden = false;
|
||||
emptyState.hidden = false;
|
||||
emptyState.textContent = "已加载输入。点击左侧“运行分割”生成导丝掩膜和叠加结果。";
|
||||
summaryStrip.hidden = true;
|
||||
videoLink.hidden = true;
|
||||
resultCount.textContent = "0 个结果";
|
||||
setCompareEnabled(false);
|
||||
}
|
||||
|
||||
function openDetail(frame) {
|
||||
@@ -174,6 +238,7 @@ function openDetail(frame) {
|
||||
}
|
||||
|
||||
function renderResults(data) {
|
||||
lastResult = data;
|
||||
lastFrames = data.frames || [];
|
||||
resultGrid.innerHTML = "";
|
||||
emptyState.hidden = lastFrames.length > 0;
|
||||
@@ -181,10 +246,19 @@ function renderResults(data) {
|
||||
if (data.video_url) {
|
||||
videoLink.href = data.video_url;
|
||||
videoLink.setAttribute("download", "");
|
||||
resultVideoPreview.preload = "metadata";
|
||||
resultVideoPreview.src = data.video_url;
|
||||
resultVideoPreview.load();
|
||||
resultVideoPreview.hidden = false;
|
||||
resultVideoEmpty.hidden = true;
|
||||
} else {
|
||||
resultVideoPreview.hidden = true;
|
||||
resultVideoPreview.removeAttribute("src");
|
||||
resultVideoEmpty.hidden = false;
|
||||
}
|
||||
updateSummary(data);
|
||||
|
||||
lastFrames.forEach((frame) => {
|
||||
lastFrames.forEach((frame, index) => {
|
||||
const node = template.content.firstElementChild.cloneNode(true);
|
||||
node.querySelector(".method").textContent = methodLabels.get(frame.method) || frame.method;
|
||||
node.querySelector(".frame-index").textContent = `帧 ${frame.frame_index}`;
|
||||
@@ -192,12 +266,16 @@ function renderResults(data) {
|
||||
node.querySelector(".coverage").textContent = `${(frame.metrics.coverage * 100).toFixed(3)}%`;
|
||||
node.querySelector(".skeleton").textContent = frame.metrics.skeleton_length;
|
||||
node.querySelector(".components").textContent = frame.metrics.components;
|
||||
node.addEventListener("click", () => openDetail(frame));
|
||||
node.dataset.frameIndex = frame.frame_index;
|
||||
node.addEventListener("click", () => selectFrame(frame, index));
|
||||
node.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") openDetail(frame);
|
||||
if (event.key === "Enter") selectFrame(frame, index);
|
||||
});
|
||||
resultGrid.appendChild(node);
|
||||
});
|
||||
if (lastFrames.length) {
|
||||
selectFrame(lastFrames[0], 0);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSample() {
|
||||
@@ -240,7 +318,11 @@ function clearAll() {
|
||||
sourceImage.hidden = true;
|
||||
sourceVideo.removeAttribute("src");
|
||||
sourceImage.removeAttribute("src");
|
||||
resultVideoPreview.hidden = true;
|
||||
resultVideoPreview.removeAttribute("src");
|
||||
resultVideoEmpty.hidden = false;
|
||||
openSourceButton.hidden = true;
|
||||
setCompareEnabled(false);
|
||||
sourcePaneTitle.textContent = "查看原始视频";
|
||||
if (sourceDialog.open) sourceDialog.close();
|
||||
previewEmpty.hidden = false;
|
||||
@@ -250,6 +332,146 @@ function clearAll() {
|
||||
videoLink.hidden = true;
|
||||
resultCount.textContent = "0 个结果";
|
||||
jobMeta.textContent = "等待输入";
|
||||
lastFrames = [];
|
||||
lastResult = null;
|
||||
currentFrame = null;
|
||||
}
|
||||
|
||||
function setCompareEnabled(enabled) {
|
||||
openCompareButton.disabled = !enabled;
|
||||
controlCompareButton.disabled = !enabled;
|
||||
}
|
||||
|
||||
function selectFrame(frame, index = 0) {
|
||||
currentFrame = frame;
|
||||
[...resultGrid.querySelectorAll(".result-card")].forEach((node) => {
|
||||
node.classList.toggle("is-selected", Number(node.dataset.frameIndex) === Number(frame.frame_index));
|
||||
});
|
||||
syncLock = true;
|
||||
seekMedia(videoPreview, frame.source_time);
|
||||
seekMedia(resultVideoPreview, frame.result_time);
|
||||
syncLock = false;
|
||||
setCompareEnabled(Boolean(selectedFile && frame));
|
||||
resultCount.textContent = `${lastFrames.length} 个结果 · 当前帧 ${frame.frame_index}`;
|
||||
}
|
||||
|
||||
function seekMedia(media, time) {
|
||||
if (!media || media.hidden || !Number.isFinite(time)) return;
|
||||
if (Math.abs(media.currentTime - time) > 0.04) {
|
||||
media.currentTime = Math.max(0, time);
|
||||
}
|
||||
}
|
||||
|
||||
function nearestFrameBy(source, time) {
|
||||
if (!lastFrames.length) return null;
|
||||
const key = source === "result" ? "result_time" : "source_time";
|
||||
return lastFrames.reduce((best, frame) => {
|
||||
const bestValue = Number.isFinite(best[key]) ? best[key] : 0;
|
||||
const value = Number.isFinite(frame[key]) ? frame[key] : 0;
|
||||
const bestDistance = Math.abs(bestValue - time);
|
||||
const distance = Math.abs(value - time);
|
||||
return distance < bestDistance ? frame : best;
|
||||
}, lastFrames[0]);
|
||||
}
|
||||
|
||||
function syncVideos(source, time) {
|
||||
if (syncLock || !lastFrames.length) return;
|
||||
const frame = nearestFrameBy(source, time);
|
||||
if (!frame) return;
|
||||
currentFrame = frame;
|
||||
syncLock = true;
|
||||
if (source === "source" && !resultVideoPreview.hidden && Number.isFinite(frame.result_time)) {
|
||||
seekMedia(resultVideoPreview, frame.result_time);
|
||||
}
|
||||
if (source === "result" && !videoPreview.hidden && Number.isFinite(frame.source_time)) {
|
||||
seekMedia(videoPreview, frame.source_time);
|
||||
}
|
||||
syncLock = false;
|
||||
[...resultGrid.querySelectorAll(".result-card")].forEach((node) => {
|
||||
node.classList.toggle("is-selected", Number(node.dataset.frameIndex) === Number(frame.frame_index));
|
||||
});
|
||||
setCompareEnabled(Boolean(selectedFile && frame));
|
||||
resultCount.textContent = `${lastFrames.length} 个结果 · 当前帧 ${frame.frame_index}`;
|
||||
}
|
||||
|
||||
function createMetricBlock(frame) {
|
||||
const metrics = document.createElement("dl");
|
||||
metrics.className = "metrics";
|
||||
[
|
||||
["覆盖率", `${(frame.metrics.coverage * 100).toFixed(3)}%`],
|
||||
["骨架", frame.metrics.skeleton_length],
|
||||
["连通域", frame.metrics.components],
|
||||
].forEach(([label, value]) => {
|
||||
const item = document.createElement("div");
|
||||
const dt = document.createElement("dt");
|
||||
const dd = document.createElement("dd");
|
||||
dt.textContent = label;
|
||||
dd.textContent = value;
|
||||
item.append(dt, dd);
|
||||
metrics.appendChild(item);
|
||||
});
|
||||
return metrics;
|
||||
}
|
||||
|
||||
function renderCompareFrames(frames) {
|
||||
compareGrid.innerHTML = "";
|
||||
frames.forEach((frame) => {
|
||||
const card = document.createElement("article");
|
||||
card.className = "compare-card";
|
||||
|
||||
const top = document.createElement("div");
|
||||
top.className = "card-top";
|
||||
const method = document.createElement("span");
|
||||
method.className = "method";
|
||||
method.textContent = methodLabels.get(frame.method) || frame.method;
|
||||
const index = document.createElement("span");
|
||||
index.className = "frame-index";
|
||||
index.textContent = `帧 ${frame.frame_index}`;
|
||||
top.append(method, index);
|
||||
|
||||
const figure = document.createElement("figure");
|
||||
const image = document.createElement("img");
|
||||
image.src = frame.overlay_url;
|
||||
image.alt = `${method.textContent} 叠加结果`;
|
||||
const caption = document.createElement("figcaption");
|
||||
caption.textContent = "当前帧叠加视图";
|
||||
figure.append(image, caption);
|
||||
|
||||
card.append(top, figure, createMetricBlock(frame));
|
||||
compareGrid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
async function openCompareForCurrentFrame() {
|
||||
if (!selectedFile || !currentFrame) return;
|
||||
setCompareEnabled(false);
|
||||
compareTitle.textContent = `当前帧 ${currentFrame.frame_index} 多方法对比`;
|
||||
compareGrid.innerHTML = '<div class="compare-loading">正在生成当前帧多方法对比。</div>';
|
||||
if (!compareDialog.open) compareDialog.showModal();
|
||||
|
||||
try {
|
||||
const payload = new FormData();
|
||||
payload.set("file", selectedFile);
|
||||
payload.set("frame_index", currentFrame.frame_index);
|
||||
payload.set("sensitivity", sensitivity.value);
|
||||
const response = await fetch("/api/compare-frame", {
|
||||
method: "POST",
|
||||
body: payload,
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.detail || "多方法对比失败");
|
||||
}
|
||||
renderCompareFrames(data.frames || []);
|
||||
} catch (error) {
|
||||
compareGrid.innerHTML = "";
|
||||
const message = document.createElement("div");
|
||||
message.className = "compare-loading";
|
||||
message.textContent = error.message || "多方法对比失败";
|
||||
compareGrid.appendChild(message);
|
||||
} finally {
|
||||
setCompareEnabled(Boolean(selectedFile && currentFrame));
|
||||
}
|
||||
}
|
||||
|
||||
sensitivity.addEventListener("input", () => {
|
||||
@@ -284,6 +506,25 @@ sampleButton.addEventListener("click", loadSample);
|
||||
clearButton.addEventListener("click", clearAll);
|
||||
closeDialog.addEventListener("click", () => detailDialog.close());
|
||||
closeSourceDialog.addEventListener("click", () => sourceDialog.close());
|
||||
closeCompareDialog.addEventListener("click", () => compareDialog.close());
|
||||
openCompareButton.addEventListener("click", openCompareForCurrentFrame);
|
||||
controlCompareButton.addEventListener("click", openCompareForCurrentFrame);
|
||||
|
||||
videoPreview.addEventListener("seeked", () => syncVideos("source", videoPreview.currentTime));
|
||||
resultVideoPreview.addEventListener("seeked", () => syncVideos("result", resultVideoPreview.currentTime));
|
||||
videoPreview.addEventListener("play", () => {
|
||||
if (!syncLock && !resultVideoPreview.hidden) resultVideoPreview.play().catch(() => {});
|
||||
});
|
||||
resultVideoPreview.addEventListener("play", () => {
|
||||
if (!syncLock && !videoPreview.hidden) videoPreview.play().catch(() => {});
|
||||
});
|
||||
videoPreview.addEventListener("pause", () => {
|
||||
if (!syncLock && !resultVideoPreview.hidden) resultVideoPreview.pause();
|
||||
});
|
||||
resultVideoPreview.addEventListener("pause", () => {
|
||||
if (!syncLock && !videoPreview.hidden) videoPreview.pause();
|
||||
});
|
||||
|
||||
openSourceButton.addEventListener("click", () => {
|
||||
if (!selectedFile || !currentObjectUrl) return;
|
||||
sourceVideo.hidden = true;
|
||||
@@ -315,9 +556,15 @@ form.addEventListener("submit", async (event) => {
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
scheduleProgress();
|
||||
setCompareEnabled(false);
|
||||
currentFrame = null;
|
||||
emptyState.hidden = false;
|
||||
emptyState.textContent = "正在抽帧和分割,请稍候。";
|
||||
resultGrid.innerHTML = "";
|
||||
resultVideoPreview.hidden = true;
|
||||
resultVideoPreview.removeAttribute("src");
|
||||
resultVideoEmpty.hidden = false;
|
||||
videoLink.hidden = true;
|
||||
|
||||
try {
|
||||
@@ -332,9 +579,11 @@ form.addEventListener("submit", async (event) => {
|
||||
throw new Error(data.detail || "分割失败");
|
||||
}
|
||||
renderResults(data);
|
||||
finishProgress();
|
||||
} catch (error) {
|
||||
emptyState.hidden = false;
|
||||
emptyState.textContent = error.message;
|
||||
failProgress(error.message || "分割失败");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
@@ -77,6 +77,9 @@
|
||||
<button class="primary" type="submit">
|
||||
<span>运行分割</span>
|
||||
</button>
|
||||
<button class="ghost compare-entry" id="controlCompareButton" type="button" disabled>
|
||||
多方法对比
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<section class="viewer">
|
||||
@@ -104,9 +107,9 @@
|
||||
<div class="pane-head">
|
||||
<div>
|
||||
<p class="eyebrow">Live Workspace</p>
|
||||
<h2>预览与结果</h2>
|
||||
<h2>预览与结果视频</h2>
|
||||
</div>
|
||||
<span id="resultCount">0 个结果</span>
|
||||
<button class="ghost media-button" id="openCompareButton" type="button" disabled>多方法对比</button>
|
||||
</div>
|
||||
|
||||
<div class="progress-wrap" id="progressWrap" hidden>
|
||||
@@ -114,6 +117,23 @@
|
||||
<p id="progressText">准备任务</p>
|
||||
</div>
|
||||
|
||||
<div class="result-video-stage">
|
||||
<div class="preview-empty" id="resultVideoEmpty">运行分割后,这里会显示叠加结果视频。</div>
|
||||
<video id="resultVideoPreview" controls muted hidden></video>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="workspace-pane frame-browser">
|
||||
<div class="pane-head">
|
||||
<div>
|
||||
<p class="eyebrow">Frame Review</p>
|
||||
<h2>预览与结果查看</h2>
|
||||
</div>
|
||||
<span id="resultCount">0 个结果</span>
|
||||
</div>
|
||||
|
||||
<div class="frame-browser-body">
|
||||
<div class="summary-strip" id="summaryStrip" hidden>
|
||||
<div><span>任务</span><strong id="summaryJob">-</strong></div>
|
||||
<div><span>帧数</span><strong id="summaryFrames">-</strong></div>
|
||||
@@ -123,8 +143,8 @@
|
||||
|
||||
<div class="empty" id="emptyState">运行分割后,这里会显示原帧、叠加图、掩膜和指标。</div>
|
||||
<div class="result-grid" id="resultGrid"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
@@ -168,6 +188,17 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog class="detail-dialog compare-dialog" id="compareDialog">
|
||||
<div class="dialog-head">
|
||||
<div>
|
||||
<p class="eyebrow">Method Compare</p>
|
||||
<h2 id="compareTitle">当前帧多方法对比</h2>
|
||||
</div>
|
||||
<button class="icon-button" id="closeCompareDialog" type="button" aria-label="关闭多方法对比">×</button>
|
||||
</div>
|
||||
<div class="compare-grid" id="compareGrid"></div>
|
||||
</dialog>
|
||||
|
||||
<template id="resultCardTemplate">
|
||||
<article class="result-card" tabindex="0">
|
||||
<div class="card-top">
|
||||
|
||||
@@ -264,7 +264,14 @@ h3 {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.ghost:hover,
|
||||
.ghost:disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--faint);
|
||||
border-color: var(--line-soft);
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.ghost:not(:disabled):hover,
|
||||
.method-option:hover,
|
||||
.result-card:hover {
|
||||
border-color: rgba(255, 209, 102, 0.62);
|
||||
@@ -284,6 +291,18 @@ h3 {
|
||||
filter: grayscale(0.45);
|
||||
}
|
||||
|
||||
.compare-entry {
|
||||
min-height: 46px;
|
||||
border-color: rgba(56, 216, 184, 0.5);
|
||||
color: var(--accent-2);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.compare-entry:disabled {
|
||||
border-color: var(--line);
|
||||
color: var(--faint);
|
||||
}
|
||||
|
||||
.method-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -419,6 +438,20 @@ input[type="range"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.result-video-stage {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 590px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(56, 216, 184, 0.035) 1px, transparent 1px),
|
||||
linear-gradient(rgba(56, 216, 184, 0.035) 1px, transparent 1px),
|
||||
#080a09;
|
||||
background-size: 28px 28px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-empty,
|
||||
.empty {
|
||||
color: var(--muted);
|
||||
@@ -427,7 +460,8 @@ input[type="range"] {
|
||||
}
|
||||
|
||||
#videoPreview,
|
||||
#imagePreview {
|
||||
#imagePreview,
|
||||
#resultVideoPreview {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: 590px;
|
||||
@@ -452,11 +486,11 @@ input[type="range"] {
|
||||
|
||||
.progress-line span {
|
||||
display: block;
|
||||
width: 12%;
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, var(--accent-2), var(--accent));
|
||||
animation: pulse-progress 1.4s ease-in-out infinite alternate;
|
||||
transition: width 320ms ease;
|
||||
}
|
||||
|
||||
.progress-wrap p {
|
||||
@@ -464,15 +498,6 @@ input[type="range"] {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@keyframes pulse-progress {
|
||||
from {
|
||||
width: 18%;
|
||||
}
|
||||
to {
|
||||
width: 92%;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@@ -515,7 +540,7 @@ input[type="range"] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
|
||||
gap: 12px;
|
||||
max-height: 590px;
|
||||
max-height: 520px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -532,6 +557,11 @@ input[type="range"] {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.result-card.is-selected {
|
||||
border-color: rgba(56, 216, 184, 0.88);
|
||||
box-shadow: inset 0 0 0 1px rgba(56, 216, 184, 0.4);
|
||||
}
|
||||
|
||||
.card-top {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
@@ -635,6 +665,62 @@ dd {
|
||||
width: min(1180px, calc(100vw - 36px));
|
||||
}
|
||||
|
||||
.frame-browser {
|
||||
min-height: auto;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.frame-browser-body {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.frame-browser .summary-strip {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.frame-browser .empty {
|
||||
min-height: 170px;
|
||||
}
|
||||
|
||||
.compare-dialog {
|
||||
width: min(1320px, calc(100vw - 36px));
|
||||
}
|
||||
|
||||
.compare-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 12px;
|
||||
max-height: min(72vh, 760px);
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.compare-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #111512;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.compare-card img {
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
|
||||
.compare-loading {
|
||||
min-height: 220px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
grid-column: 1 / -1;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: rgba(16, 20, 17, 0.72);
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.source-stage {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -683,6 +769,10 @@ dd {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.frame-browser .summary-strip {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.viewer-split {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -692,6 +782,7 @@ dd {
|
||||
}
|
||||
|
||||
.preview-stage,
|
||||
.result-video-stage,
|
||||
.empty {
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user