import { esc, icon, external, section } from "./ui.js"; export function previewHTML(dataset, context) { const previews = context.previews[dataset.id] || []; if (!previews.length) return ""; return `
${icon("chevron-left", "上一个样例", "data-prev")} ${icon("chevron-right", "下一个样例", "data-next")}
原始图像
来源标注加载中
${icon("scan", "定位手部", "data-hand")}${icon("maximize", "显示全图", "data-reset")}
${previews.map((p, i) => ``).join("")}
来源与抽样
${samplingHTML(dataset.id, context.sampling)}
`; } export function samplingHTML(id, sampling) { const sample = sampling.datasets?.[id]; if (!sample) return ""; return `
${Object.entries(sample.categories) .map( ([dimension, groups]) => `
${esc(dimension)}
${Object.entries(groups) .map(([name, n]) => `${esc(name)} ${n.selected}/${n.pool}`) .join(" · ")}
`, ) .join( "", )}

${esc(sampling.strategy)} ${esc(sampling.dimensions_note)}

`; } export function bindPreview(dataset, context) { const root = document.getElementById("dataset-preview"); if (!root) return; const previews = context.previews[dataset.id]; const find = (name) => root.querySelector(`[data-${name}]`); let index = 0, box, version = 0; function viewport() { const zoom = Number(find("zoom").value); const w = box[2] / zoom, h = box[3] / zoom; const viewBox = [ box[0] + (box[2] - w) / 2, box[1] + (box[3] - h) / 2, w, h, ].join(" "); find("left").setAttribute("viewBox", viewBox); find("right").setAttribute("viewBox", viewBox); find("zoom-value").textContent = `${Number(zoom.toFixed(1))}×`; } async function annotation() { const sample = previews[index], mode = sample.modes[Number(find("mode").value)]; const own = ++version, pane = find("right"), loading = find("loading"); find("gt-label").textContent = mode.label + (dataset.method === "pseudo" ? " · 预测标注" : ""); loading.hidden = false; loading.textContent = "加载中"; pane.setAttribute("aria-busy", "true"); const images = []; try { await Promise.all( [ ["base", sample.rgb], ["overlay", mode.src], ].map( ([name, url]) => new Promise((resolve, reject) => { const image = document.createElementNS( "http://www.w3.org/2000/svg", "image", ); image.dataset[name] = ""; image.setAttribute("width", sample.width); image.setAttribute("height", sample.height); image.style.visibility = "hidden"; if (name === "overlay") image.style.opacity = Number(find("alpha").value) / 100; image.onload = resolve; image.onerror = reject; find(name).replaceWith(image); images.push(image); image.setAttribute("href", url); }), ), ); if (own !== version || !root.isConnected) return; images.forEach((image) => { image.style.visibility = "visible"; }); loading.hidden = true; } catch (_) { if (own !== version || !root.isConnected) return; loading.textContent = "图像加载失败"; } finally { if (own === version && root.isConnected) pane.setAttribute("aria-busy", "false"); } } function show() { const sample = previews[index]; find("sample").value = index; for (const [name, value] of [ ["width", sample.width], ["height", sample.height], ["href", sample.rgb], ]) find("rgb").setAttribute(name, value); find("rgb-label").textContent = sample.rgb_label; find("mode").innerHTML = sample.modes .map((m, i) => ``) .join(""); annotation(); box = [0, 0, sample.width, sample.height]; find("zoom").value = 1; viewport(); find("hand").hidden = !sample.crop; find("source").innerHTML = `

${esc(sample.source)}

${sample.note ? `

${esc(sample.note)}

` : ""}

${Object.entries( sample.strata || {}, ) .map(([k, v]) => `${esc(k)}:${esc(v)}`) .join( " · ", )}

${sample.license ? `

${esc(sample.license)}

` : ""}`; root .querySelectorAll("[data-preview]") .forEach((button) => button.setAttribute( "aria-pressed", Number(button.dataset.preview) === index, ), ); } find("prev").onclick = () => { index = (index + previews.length - 1) % previews.length; show(); }; find("next").onclick = () => { index = (index + 1) % previews.length; show(); }; find("sample").onchange = (event) => { index = Number(event.target.value); show(); }; find("mode").onchange = annotation; find("alpha").oninput = (event) => { find("overlay").style.opacity = event.target.value / 100; find("alpha-value").textContent = `${event.target.value}%`; }; find("zoom").oninput = viewport; find("hand").onclick = () => { box = previews[index].crop; find("zoom").value = 1; viewport(); }; find("reset").onclick = () => { box = [0, 0, previews[index].width, previews[index].height]; find("zoom").value = 1; viewport(); }; root.querySelectorAll("[data-preview]").forEach((button) => { button.onclick = () => { index = Number(button.dataset.preview); show(); }; }); show(); } export function figuresHTML(id, context) { const entry = context.figures[id]; if (!entry?.figures?.length) return ""; return section( "采集与标注", `${entry.note ? `

${esc(entry.note)}

` : ""}
${entry.figures.map((figure) => `

${esc(figure.title)}

${figure.images.map((img) => `${esc(figure.title)}`).join("")}
${esc(figure.caption)} ${external(figure.source_label, figure.source_url)}
原文与署名

${esc(figure.original_caption)}

${esc(figure.attribution)}

`).join("")}
`, ); } export function videoHTML(id, context) { const list = context.videos[id] || []; if (!list.length) return ""; return section( "动作序列", `

`, ); } export function bindVideo(id, context) { const select = document.getElementById("clip-select"); if (!select) return; const player = document.getElementById("clip-player"); const update = () => { const video = context.videos[id][Number(select.value)]; player.pause(); player.src = video.url; document.getElementById("clip-note").textContent = `${video.source_tag} · ${video.mano_source} · 来源帧 ${video.source_first_frame}–${video.source_last_frame} · ${video.duration_s}秒`; }; select.onchange = update; update(); }