Files
2026-05-30 19:32:01 +07:00

255 lines
6.7 KiB
HTML

<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
body {
margin: 0;
background: #111;
color: white;
font-family: sans-serif;
overflow: auto;
}
.bar {
position: sticky;
top: 0;
z-index: 10;
background: #000c;
padding: 8px;
display: flex;
gap: 12px;
align-items: center;
}
button,
select,
input {
font: inherit;
}
canvas {
display: block;
margin: 12px auto;
background: #000;
}
</style>
</head>
<body>
<div class="bar">
<button id="refresh">refresh</button>
<label>
mode
<select id="mode">
<option value="edge">edge diff</option>
<option value="diff">raw diff</option>
<option value="overlay">red/green overlay</option>
</select>
</label>
<label>
threshold
<input id="threshold" type="range" min="0" max="120" value="85" />
<span id="thresholdText">85</span>
</label>
<label>
time: <span id="timingText">Loading...</span
><span style="color: gray">(image load complete in browser)</span>
</label>
</div>
<canvas id="canvas" width="1870" height="778"></canvas>
<script>
const urlParams = new URLSearchParams(window.location.search);
const type = urlParams.get("type");
let MINE, ORIG, W, H;
if (type === "list") {
MINE = "/v1/card/genshin/775956514";
ORIG = "/v1/card/genshin/diff/listoriginal.webp";
W = 1200;
H = 732;
} else {
MINE = "/v1/card/genshin/775956514/ororon";
ORIG = "/v1/card/genshin/diff/ororo.png";
W = 1870;
H = 778;
}
/**
* @type {HTMLCanvasElement}
*/
const canvas = document.getElementById("canvas");
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext("2d", { willReadFrequently: true });
const mode = document.getElementById("mode");
const threshold = document.getElementById("threshold");
const thresholdText = document.getElementById("thresholdText");
const timingText = document.getElementById("timingText");
let mineData,
origData,
refreshing = false;
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => resolve(img);
img.onerror = reject;
img.src =
src + (src.includes("?") ? "&" : "?") + "cacheBust=" + Date.now();
});
}
function drawToData(img) {
const c = document.createElement("canvas");
c.width = W;
c.height = H;
const x = c.getContext("2d", { willReadFrequently: true });
x.drawImage(img, 0, 0, W, H);
return x.getImageData(0, 0, W, H);
}
async function refresh() {
if (refreshing) return;
refreshing = true;
ctx.fillStyle = "#00000088";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "white";
let line = 30;
function log(text) {
ctx.fillText(text, 20, line);
line += 16;
}
log("fetching...");
const start = Date.now();
const [mineImg, origImg] = await Promise.all([
loadImage(MINE).finally(
() => (timingText.innerText = `${Date.now() - start}ms`),
),
loadImage(ORIG),
]);
log("loading...");
mineData = drawToData(mineImg);
if (!origData) origData = drawToData(origImg);
log("rendering...");
render();
refreshing = false;
}
function lum(d, i) {
return d[i] * 0.299 + d[i + 1] * 0.587 + d[i + 2] * 0.114;
}
function edgeAt(data, x, y) {
const d = data.data;
const i = (y * W + x) * 4;
const l = lum(d, i);
const lx = x > 0 ? lum(d, i - 4) : l;
const rx = x < W - 1 ? lum(d, i + 4) : l;
const uy = y > 0 ? lum(d, i - W * 4) : l;
const dy = y < H - 1 ? lum(d, i + W * 4) : l;
return Math.abs(rx - lx) + Math.abs(dy - uy);
}
function render() {
if (!mineData || !origData) return;
const out = ctx.createImageData(W, H);
const o = out.data;
const m = mineData.data;
const r = origData.data;
const t = Number(threshold.value);
thresholdText.textContent = t;
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
const i = (y * W + x) * 4;
if (mode.value === "edge") {
const me = edgeAt(mineData, x, y);
const oe = edgeAt(origData, x, y);
const mineEdge = me > t;
const origEdge = oe > t;
if (mineEdge && origEdge) {
// matching = green
o[i] = 0;
o[i + 1] = 255;
o[i + 2] = 0;
o[i + 3] = 255;
} else if (origEdge) {
// original missing from mine = red
o[i] = 255;
o[i + 1] = 0;
o[i + 2] = 0;
o[i + 3] = 255;
} else if (mineEdge) {
// extra in mine = yellow
o[i] = 255;
o[i + 1] = 255;
o[i + 2] = 0;
o[i + 3] = 255;
} else {
o[i + 3] = 0;
}
} else if (mode.value === "diff") {
const dr = Math.abs(m[i] - r[i]);
const dg = Math.abs(m[i + 1] - r[i + 1]);
const db = Math.abs(m[i + 2] - r[i + 2]);
const diff = Math.max(dr, dg, db);
o[i] = diff;
o[i + 1] = diff;
o[i + 2] = diff;
o[i + 3] = diff > t ? 255 : 0;
} else {
o[i] = m[i]; // mine red-ish
o[i + 1] = r[i + 1]; // original green-ish
o[i + 2] = 0;
o[i + 3] = 220;
}
}
}
ctx.putImageData(out, 0, 0);
}
document.getElementById("refresh").onclick = refresh;
mode.oninput = render;
threshold.oninput = render;
refresh().catch((err) => {
console.error(err);
ctx.fillStyle = "red";
ctx.fillText(String(err), 20, 30);
});
window.addEventListener("keydown", (e) => {
if (
e.key === "Enter" &&
!["INPUT", "SELECT"].includes(e.target.tagName)
) {
e.preventDefault();
refresh();
}
});
// window.addEventListener("focus", () => {
// refresh();
// });
</script>
</body>
</html>