File size: 9,323 Bytes
831819e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | import { esc, icon, external, section } from "./ui.js";
export function previewHTML(dataset, context) {
const previews = context.previews[dataset.id] || [];
if (!previews.length) return "";
return `<section class="preview" id="dataset-preview" aria-label="${esc(dataset.name)}样例">
<div class="preview-toolbar">${icon("chevron-left", "上一个样例", "data-prev")}
<select data-sample aria-label="样例">${previews.map((p, i) => `<option value="${i}">${i + 1} · ${esc(p.caption)}</option>`).join("")}</select>
${icon("chevron-right", "下一个样例", "data-next")}
<select data-mode aria-label="标注类型"></select>
</div>
<div class="preview-pair">
<figure><figcaption data-rgb-label>原始图像</figcaption><svg data-left role="img" aria-label="原始图像" preserveAspectRatio="xMidYMid meet"><image data-rgb/></svg></figure>
<figure><figcaption><span data-gt-label>来源标注</span><span data-loading role="status" aria-live="polite">加载中</span></figcaption><svg data-right role="img" aria-label="来源标注叠加" preserveAspectRatio="xMidYMid meet"><image data-base/><image data-overlay/></svg></figure>
</div>
<div class="preview-adjustments">
<label>透明度<input data-alpha aria-label="叠加透明度" type="range" min="0" max="100" value="100"><output data-alpha-value>100%</output></label>
<label>缩放<input data-zoom aria-label="同步缩放" type="range" min="1" max="8" step="0.1" value="1"><output data-zoom-value>1×</output></label>
<div class="button-group">${icon("scan", "定位手部", "data-hand")}${icon("maximize", "显示全图", "data-reset")}</div>
</div>
<div class="preview-thumbs" aria-label="选择样例">${previews.map((p, i) => `<button type="button" data-preview="${i}" aria-label="样例${i + 1}"><img src="${esc(p.rgb)}" alt="" loading="lazy"><span>${i + 1}</span></button>`).join("")}</div>
<details class="provenance"><summary>来源与抽样</summary><div data-source class="source-content"></div>${samplingHTML(dataset.id, context.sampling)}</details>
</section>`;
}
export function samplingHTML(id, sampling) {
const sample = sampling.datasets?.[id];
if (!sample) return "";
return `<dl class="facts compact">${Object.entries(sample.categories)
.map(
([dimension, groups]) =>
`<div><dt>${esc(dimension)}</dt><dd>${Object.entries(groups)
.map(([name, n]) => `${esc(name)} ${n.selected}/${n.pool}`)
.join(" · ")}</dd></div>`,
)
.join(
"",
)}</dl><p class="source-note">${esc(sampling.strategy)} ${esc(sampling.dimensions_note)}</p>`;
}
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) => `<option value="${i}">${esc(m.label)}</option>`)
.join("");
annotation();
box = [0, 0, sample.width, sample.height];
find("zoom").value = 1;
viewport();
find("hand").hidden = !sample.crop;
find("source").innerHTML =
`<p>${esc(sample.source)}</p>${sample.note ? `<p>${esc(sample.note)}</p>` : ""}<p>${Object.entries(
sample.strata || {},
)
.map(([k, v]) => `${esc(k)}:${esc(v)}`)
.join(
" · ",
)}</p><div class="link-list">${sample.source_page ? external("来源图册", sample.source_page) : ""}${sample.evidence_url ? external("素材记录", sample.evidence_url) : ""}${external("原图", sample.rgb)}</div>${sample.license ? `<p>${esc(sample.license)}</p>` : ""}`;
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 ? `<p class="source-note">${esc(entry.note)}</p>` : ""}<div class="official-figures">${entry.figures.map((figure) => `<figure><h3>${esc(figure.title)}</h3><div class="figure-images">${figure.images.map((img) => `<a href="${esc(img.src)}" target="_blank" rel="noopener"><img src="${esc(img.src)}" alt="${esc(figure.title)}" width="${img.width}" height="${img.height}" loading="lazy"></a>`).join("")}</div><figcaption>${esc(figure.caption)} ${external(figure.source_label, figure.source_url)}</figcaption><details><summary>原文与署名</summary><p>${esc(figure.original_caption)}</p><p>${esc(figure.attribution)}</p></details></figure>`).join("")}</div>`,
);
}
export function videoHTML(id, context) {
const list = context.videos[id] || [];
if (!list.length) return "";
return section(
"动作序列",
`<select id="clip-select" aria-label="视频序列">${list.map((v, i) => `<option value="${i}">${esc(v.recording_id)} · ${esc(v.source_tag)}</option>`).join("")}</select><video id="clip-player" controls playsinline preload="none"></video><p id="clip-note" class="source-note"></p>`,
);
}
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();
}
|