You've already forked lubo_comment_query
145 lines
5.5 KiB
JavaScript
145 lines
5.5 KiB
JavaScript
/**
|
|
* 视频右侧栏 OCR 结果 canvas 复刻绘制。
|
|
* 数据源:CDN 上的分P全量 JSON({space:[w,h], recs:[[ts,[[x1,y1,x2,y2,text],...]],...]})
|
|
* 或服务器注入的单帧快照({ts, lines:[[x1,y1,x2,y2,text],...]})。
|
|
*/
|
|
(function () {
|
|
// 按框坐标 1:1 复刻:黑底、黄框、白字,字号按框高自适应
|
|
function drawFrame(canvas, space, lines) {
|
|
const sw = space[0], sh = space[1];
|
|
const cssWidth = canvas.clientWidth || 200;
|
|
const scale = cssWidth / sw;
|
|
const cssHeight = Math.max(1, Math.round(sh * scale));
|
|
const dpr = window.devicePixelRatio || 1;
|
|
canvas.width = Math.round(cssWidth * dpr);
|
|
canvas.height = Math.round(cssHeight * dpr);
|
|
canvas.style.height = cssHeight + "px";
|
|
const ctx = canvas.getContext("2d");
|
|
ctx.setTransform(dpr * scale, 0, 0, dpr * scale, 0, 0);
|
|
ctx.fillStyle = "#000000";
|
|
ctx.fillRect(0, 0, sw, sh);
|
|
ctx.strokeStyle = "#eab308";
|
|
ctx.fillStyle = "#ffffff";
|
|
ctx.textBaseline = "middle";
|
|
lines.forEach(function (line) {
|
|
const x1 = line[0], y1 = line[1], x2 = line[2], y2 = line[3], text = line[4];
|
|
ctx.lineWidth = 2 / scale;
|
|
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
|
|
if (text) {
|
|
ctx.font = Math.max(4, (y2 - y1) * 0.75) + "px sans-serif";
|
|
ctx.fillText(text, x1 + 2 / scale, (y1 + y2) / 2, x2 - x1 - 4 / scale);
|
|
}
|
|
});
|
|
}
|
|
|
|
function formatHms(seconds) {
|
|
seconds = Math.max(0, Math.floor(seconds));
|
|
const h = Math.floor(seconds / 3600);
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
const s = seconds % 60;
|
|
const pad = (n) => String(n).padStart(2, "0");
|
|
return h + ":" + pad(m) + ":" + pad(s);
|
|
}
|
|
|
|
// 单帧 space:按本帧最大框坐标向上取整到 16 的倍数(与导出端一致)
|
|
function frameSpace(lines) {
|
|
let maxX = 0, maxY = 0;
|
|
lines.forEach(function (line) {
|
|
maxX = Math.max(maxX, line[2]);
|
|
maxY = Math.max(maxY, line[3]);
|
|
});
|
|
return [Math.max(16, Math.ceil(maxX / 16) * 16), Math.max(16, Math.ceil(maxY / 16) * 16)];
|
|
}
|
|
|
|
// 稿件详情页:CDN 全量 + 进度条联动(展示 ts<=t 最近一条)
|
|
function initPlayer(panel) {
|
|
const urls = JSON.parse(panel.dataset.urls || "{}");
|
|
const canvas = panel.querySelector("canvas");
|
|
const slider = panel.querySelector("input[type=range]");
|
|
const timeLabel = panel.querySelector("[data-ocr-time]");
|
|
const status = panel.querySelector("[data-ocr-status]");
|
|
const tabs = panel.querySelectorAll("[data-ocr-part]");
|
|
let data = null;
|
|
let loadSeq = 0;
|
|
|
|
function showStatus(text) {
|
|
if (status) {
|
|
status.textContent = text;
|
|
status.style.display = text ? "" : "none";
|
|
}
|
|
}
|
|
|
|
function currentIndex(t) {
|
|
// 二分:ts<=t 的最后一条;t 早于首条时展示首条
|
|
const recs = data.recs;
|
|
let lo = 0, hi = recs.length - 1, ans = 0;
|
|
while (lo <= hi) {
|
|
const mid = (lo + hi) >> 1;
|
|
if (recs[mid][0] <= t) { ans = mid; lo = mid + 1; } else { hi = mid - 1; }
|
|
}
|
|
return ans;
|
|
}
|
|
|
|
function render() {
|
|
if (!data || data.recs.length === 0) {
|
|
return;
|
|
}
|
|
const t = parseFloat(slider.value);
|
|
const idx = currentIndex(t);
|
|
timeLabel.textContent = formatHms(t) + "(OCR @" + formatHms(data.recs[idx][0]) + ")";
|
|
drawFrame(canvas, data.space, data.recs[idx][1]);
|
|
}
|
|
|
|
function load(partNum) {
|
|
const seq = ++loadSeq;
|
|
data = null;
|
|
showStatus("加载中…");
|
|
tabs.forEach(function (tab) {
|
|
tab.classList.toggle("font-bold", tab.dataset.ocrPart === String(partNum));
|
|
});
|
|
fetch(urls[partNum])
|
|
.then((response) => {
|
|
if (!response.ok) { throw new Error("HTTP " + response.status); }
|
|
return response.json();
|
|
})
|
|
.then((payload) => {
|
|
if (seq !== loadSeq) { return; }
|
|
data = payload;
|
|
slider.max = payload.recs.length ? Math.floor(payload.recs[payload.recs.length - 1][0]) : 0;
|
|
showStatus("");
|
|
render();
|
|
})
|
|
.catch(() => {
|
|
if (seq !== loadSeq) { return; }
|
|
showStatus("OCR 数据加载失败");
|
|
});
|
|
}
|
|
|
|
slider.addEventListener("input", render);
|
|
tabs.forEach(function (tab) {
|
|
tab.addEventListener("click", function () {
|
|
slider.value = 0;
|
|
load(tab.dataset.ocrPart);
|
|
});
|
|
});
|
|
const firstPart = Object.keys(urls)[0];
|
|
if (firstPart !== undefined) {
|
|
load(firstPart);
|
|
}
|
|
}
|
|
|
|
// 节目详情页:服务器注入的单帧快照
|
|
function initSnapshots(scope) {
|
|
(scope || document).querySelectorAll("canvas[data-ocr-frame]").forEach(function (canvas) {
|
|
const frame = JSON.parse(canvas.dataset.ocrFrame);
|
|
drawFrame(canvas, frameSpace(frame.lines), frame.lines);
|
|
});
|
|
}
|
|
|
|
window.OcrCanvas = {
|
|
drawFrame: drawFrame,
|
|
initPlayer: initPlayer,
|
|
initSnapshots: initSnapshots,
|
|
};
|
|
})();
|