Files
lubo_comment_query/resources/js/component/ocr_canvas.js
T
2026-07-29 10:40:52 +08:00

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,
};
})();