mirror of
https://github.com/vibegif/vibegif.lol.git
synced 2026-04-07 02:12:12 +00:00
Feat: background-removal test playground
This commit is contained in:
245
test.html
Normal file
245
test.html
Normal file
@@ -0,0 +1,245 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>vibegif.lol — background-removal test</title>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Stain";
|
||||
src: url("https://cdn.jsdelivr.net/gh/multipleof4/stain.otf@master/dist/Stain.otf") format("opentype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
body, input, select, button, a, label {
|
||||
font-family: "Stain", sans-serif;
|
||||
}
|
||||
.checker {
|
||||
background-image:
|
||||
linear-gradient(45deg, #eee 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #eee 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #eee 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #eee 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0;
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-white text-neutral-800 min-h-screen">
|
||||
<main class="max-w-5xl mx-auto px-4 py-8 flex flex-col gap-6">
|
||||
<header class="flex items-center justify-between">
|
||||
<h1 class="text-4xl tracking-tight">bg test<span class="text-neutral-400">.lol</span></h1>
|
||||
<a href="/" class="text-sm underline text-neutral-500 hover:text-neutral-800">back to vibegif</a>
|
||||
</header>
|
||||
|
||||
<p class="text-neutral-500">
|
||||
Upload image(s), run <code>@imgly/background-removal</code>, compare results. First run may take longer (model download).
|
||||
</p>
|
||||
|
||||
<section class="grid md:grid-cols-4 gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-neutral-400 uppercase tracking-wider">Files</label>
|
||||
<input id="inp-files" type="file" accept="image/*" multiple class="border border-neutral-300 rounded-lg px-3 py-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-neutral-400 uppercase tracking-wider">Output Type</label>
|
||||
<select id="sel-type" class="border border-neutral-300 rounded-lg px-3 py-2 bg-white">
|
||||
<option value="foreground">foreground</option>
|
||||
<option value="mask">mask</option>
|
||||
<option value="background">background</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-neutral-400 uppercase tracking-wider">Format</label>
|
||||
<select id="sel-format" class="border border-neutral-300 rounded-lg px-3 py-2 bg-white">
|
||||
<option value="image/png">image/png</option>
|
||||
<option value="image/webp">image/webp</option>
|
||||
<option value="image/jpeg">image/jpeg</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-neutral-400 uppercase tracking-wider">Quality (0-1)</label>
|
||||
<input id="inp-quality" type="number" min="0" max="1" step="0.05" value="0.9" class="border border-neutral-300 rounded-lg px-3 py-2" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-wrap items-center gap-3">
|
||||
<button id="btn-run" class="bg-neutral-800 text-white rounded-lg px-5 py-3 hover:bg-neutral-700 transition">remove background</button>
|
||||
<button id="btn-clear" class="border border-neutral-300 rounded-lg px-5 py-3 hover:bg-neutral-100 transition">clear</button>
|
||||
<p id="status" class="text-sm text-neutral-500"></p>
|
||||
</section>
|
||||
|
||||
<section id="results" class="grid md:grid-cols-2 gap-4"></section>
|
||||
</main>
|
||||
|
||||
<script type="module">
|
||||
import removeBackground from "https://esm.sh/@imgly/background-removal@1.7.0";
|
||||
|
||||
const el = {
|
||||
files: document.getElementById("inp-files"),
|
||||
type: document.getElementById("sel-type"),
|
||||
format: document.getElementById("sel-format"),
|
||||
quality: document.getElementById("inp-quality"),
|
||||
run: document.getElementById("btn-run"),
|
||||
clear: document.getElementById("btn-clear"),
|
||||
status: document.getElementById("status"),
|
||||
results: document.getElementById("results")
|
||||
};
|
||||
|
||||
const objectUrls = new Set();
|
||||
|
||||
const setStatus = (msg) => {
|
||||
el.status.textContent = msg || "";
|
||||
};
|
||||
|
||||
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
||||
|
||||
const toDownloadExt = (mime) => {
|
||||
if (mime === "image/webp") return "webp";
|
||||
if (mime === "image/jpeg") return "jpg";
|
||||
return "png";
|
||||
};
|
||||
|
||||
const makeCard = ({ name, originalUrl, outputUrl, outputMime }, idx) => {
|
||||
const card = document.createElement("article");
|
||||
card.className = "border border-neutral-200 rounded-xl p-4 flex flex-col gap-3";
|
||||
|
||||
const title = document.createElement("h2");
|
||||
title.className = "text-lg";
|
||||
title.textContent = `${idx + 1}. ${name}`;
|
||||
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "grid grid-cols-2 gap-3";
|
||||
|
||||
const left = document.createElement("div");
|
||||
left.className = "flex flex-col gap-2";
|
||||
|
||||
const leftLabel = document.createElement("p");
|
||||
leftLabel.className = "text-xs text-neutral-400 uppercase tracking-wider";
|
||||
leftLabel.textContent = "original";
|
||||
|
||||
const leftImg = document.createElement("img");
|
||||
leftImg.src = originalUrl;
|
||||
leftImg.className = "w-full h-56 object-contain rounded-lg border border-neutral-200 bg-white";
|
||||
|
||||
left.append(leftLabel, leftImg);
|
||||
|
||||
const right = document.createElement("div");
|
||||
right.className = "flex flex-col gap-2";
|
||||
|
||||
const rightLabel = document.createElement("p");
|
||||
rightLabel.className = "text-xs text-neutral-400 uppercase tracking-wider";
|
||||
rightLabel.textContent = "background removed (checkerboard)";
|
||||
|
||||
const checker = document.createElement("div");
|
||||
checker.className = "checker rounded-lg border border-neutral-200 h-56 flex items-center justify-center overflow-hidden";
|
||||
|
||||
const rightImg = document.createElement("img");
|
||||
rightImg.src = outputUrl;
|
||||
rightImg.className = "max-w-full max-h-full object-contain";
|
||||
checker.appendChild(rightImg);
|
||||
|
||||
right.append(rightLabel, checker);
|
||||
|
||||
wrap.append(left, right);
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "flex items-center gap-3";
|
||||
|
||||
const dl = document.createElement("a");
|
||||
dl.href = outputUrl;
|
||||
dl.download = `${name.replace(/\.[^.]+$/, "")}-nobg.${toDownloadExt(outputMime)}`;
|
||||
dl.className = "bg-neutral-800 text-white rounded-lg px-4 py-2 hover:bg-neutral-700 transition no-underline";
|
||||
dl.textContent = "download output";
|
||||
|
||||
actions.append(dl);
|
||||
card.append(title, wrap, actions);
|
||||
return card;
|
||||
};
|
||||
|
||||
const revokeAllUrls = () => {
|
||||
for (const url of objectUrls) URL.revokeObjectURL(url);
|
||||
objectUrls.clear();
|
||||
};
|
||||
|
||||
const clearResults = () => {
|
||||
revokeAllUrls();
|
||||
el.results.innerHTML = "";
|
||||
setStatus("");
|
||||
};
|
||||
|
||||
const setBusy = (busy) => {
|
||||
el.run.disabled = busy;
|
||||
el.run.style.opacity = busy ? "0.5" : "1";
|
||||
el.run.style.cursor = busy ? "not-allowed" : "pointer";
|
||||
};
|
||||
|
||||
el.clear.addEventListener("click", clearResults);
|
||||
|
||||
el.run.addEventListener("click", async () => {
|
||||
const files = [...(el.files.files || [])];
|
||||
if (!files.length) {
|
||||
setStatus("pick at least one image first.");
|
||||
el.files.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
clearResults();
|
||||
setBusy(true);
|
||||
|
||||
try {
|
||||
const outputType = el.type.value;
|
||||
const outputFormat = el.format.value;
|
||||
const quality = clamp(Number.parseFloat(el.quality.value || "0.9") || 0.9, 0, 1);
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
setStatus(`processing ${i + 1}/${files.length}: ${file.name} ...`);
|
||||
|
||||
const config = {
|
||||
output: {
|
||||
type: outputType,
|
||||
format: outputFormat,
|
||||
quality
|
||||
},
|
||||
progress: (key, current, total) => {
|
||||
if (!total) return;
|
||||
const pct = Math.round((current / total) * 100);
|
||||
setStatus(`downloading model assets (${key}) ${pct}% — file ${i + 1}/${files.length}`);
|
||||
}
|
||||
};
|
||||
|
||||
const outBlob = await removeBackground(file, config);
|
||||
|
||||
const originalUrl = URL.createObjectURL(file);
|
||||
const outputUrl = URL.createObjectURL(outBlob);
|
||||
objectUrls.add(originalUrl);
|
||||
objectUrls.add(outputUrl);
|
||||
|
||||
const card = makeCard({
|
||||
name: file.name,
|
||||
originalUrl,
|
||||
outputUrl,
|
||||
outputMime: outBlob.type || outputFormat
|
||||
}, i);
|
||||
|
||||
el.results.appendChild(card);
|
||||
}
|
||||
|
||||
setStatus(`done ✓ processed ${files.length} image${files.length > 1 ? "s" : ""}.`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setStatus(`error: ${err?.message || String(err)}`);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", revokeAllUrls);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user