216 lines
5.5 KiB
HTML
216 lines
5.5 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;
|
|
width: 1870px;
|
|
height: 778px;
|
|
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>
|
|
</div>
|
|
|
|
<canvas id="canvas" width="1870" height="778"></canvas>
|
|
|
|
<script>
|
|
const MINE = "/v1/card/genshin/775956514/ororon";
|
|
const ORIG = "https://media.discordapp.net/attachments/1234762106558218332/1500814127566684202/81a07207-4f8e-4adb-a3c3-dea113477246.png?ex=69f9cd93&is=69f87c13&hm=7708cde2a1161a7d0b26655902d29f93b297e54293725912e981e3e0701b37eb&=&format=webp&quality=lossless&width=1870&height=778";
|
|
|
|
const W = 1870, H = 778;
|
|
/**
|
|
* @type {HTMLCanvasElement}
|
|
*/
|
|
const canvas = document.getElementById("canvas");
|
|
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
|
|
const mode = document.getElementById("mode");
|
|
const threshold = document.getElementById("threshold");
|
|
const thresholdText = document.getElementById("thresholdText");
|
|
|
|
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";
|
|
ctx.fillText("fetching...", 20, 30);
|
|
|
|
const [mineImg, origImg] = await Promise.all([
|
|
loadImage(MINE),
|
|
loadImage(ORIG),
|
|
]);
|
|
|
|
ctx.fillText("loading...", 20, 46);
|
|
mineData = drawToData(mineImg);
|
|
if (!origData) origData = drawToData(origImg);
|
|
|
|
ctx.fillText("rendering...", 20, 62);
|
|
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 mr = me > t ? 255 : 0;
|
|
const og = oe > t ? 255 : 0;
|
|
|
|
o[i] = mr; // mine = red
|
|
o[i + 1] = og; // original = green
|
|
o[i + 2] = 0;
|
|
o[i + 3] = Math.max(mr, og);
|
|
}
|
|
|
|
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);
|
|
});
|
|
|
|
// ENTER = refresh (anywhere, unless typing in input)
|
|
window.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter" && !["INPUT", "SELECT"].includes(e.target.tagName)) {
|
|
e.preventDefault();
|
|
refresh();
|
|
}
|
|
});
|
|
|
|
// WINDOW FOCUS = refresh
|
|
window.addEventListener("focus", () => {
|
|
refresh();
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html> |