Spaces:
Sleeping
Sleeping
akswaroop commited on
Commit ·
2d0208d
1
Parent(s): 089564d
fix: auto-detect server URL, works on HuggingFace
Browse files- templates/index.html +32 -118
templates/index.html
CHANGED
|
@@ -14,7 +14,6 @@
|
|
| 14 |
--grid:repeating-conic-gradient(#1c1c2880 0% 25%,#13131a 0% 50%) 0 0/20px 20px;
|
| 15 |
}
|
| 16 |
body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden}
|
| 17 |
-
/* NAV */
|
| 18 |
nav{display:flex;align-items:center;justify-content:space-between;padding:16px 36px;border-bottom:1px solid var(--border);position:sticky;top:0;background:rgba(10,10,15,0.9);backdrop-filter:blur(20px);z-index:100}
|
| 19 |
.logo{font-family:'Syne',sans-serif;font-weight:800;font-size:21px;display:flex;align-items:center;gap:10px}
|
| 20 |
.logo-badge{background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
|
@@ -22,7 +21,6 @@ nav{display:flex;align-items:center;justify-content:space-between;padding:16px 3
|
|
| 22 |
.status-dot{width:7px;height:7px;border-radius:50%;background:var(--border);transition:.3s}
|
| 23 |
.status-dot.online{background:var(--success);box-shadow:0 0 6px var(--success)}
|
| 24 |
.status-dot.offline{background:var(--danger)}
|
| 25 |
-
/* HERO */
|
| 26 |
.hero{text-align:center;padding:48px 20px 36px;position:relative}
|
| 27 |
.hero::before{content:'';position:absolute;width:500px;height:500px;border-radius:50%;background:radial-gradient(circle,rgba(124,92,252,0.1) 0%,transparent 70%);top:50%;left:50%;transform:translate(-50%,-50%);pointer-events:none}
|
| 28 |
.hero h1{font-family:'Syne',sans-serif;font-size:clamp(32px,5.5vw,64px);font-weight:800;line-height:1.05;letter-spacing:-2px;margin-bottom:12px}
|
|
@@ -31,19 +29,7 @@ nav{display:flex;align-items:center;justify-content:space-between;padding:16px 3
|
|
| 31 |
.tech-row{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}
|
| 32 |
.tech-pill{padding:5px 14px;border-radius:50px;border:1px solid var(--border);background:var(--surface);color:var(--muted);font-size:12px;font-weight:500}
|
| 33 |
.tech-pill.active{border-color:var(--accent);color:var(--accent);background:rgba(124,92,252,0.08)}
|
| 34 |
-
/* WORKSPACE */
|
| 35 |
.workspace{max-width:1080px;margin:0 auto;padding:0 20px 60px}
|
| 36 |
-
/* SERVER BANNER */
|
| 37 |
-
.server-banner{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:14px 18px;border-radius:14px;border:1px solid var(--border);background:var(--surface);margin-bottom:24px;flex-wrap:wrap}
|
| 38 |
-
.sb-left{display:flex;align-items:center;gap:10px}
|
| 39 |
-
.sb-icon{font-size:20px}
|
| 40 |
-
.sb-text{font-size:14px;color:var(--muted)}
|
| 41 |
-
.sb-text strong{color:var(--text)}
|
| 42 |
-
.server-url{flex:1;min-width:180px;max-width:300px;padding:8px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;font-family:'DM Sans',sans-serif}
|
| 43 |
-
.server-url:focus{outline:none;border-color:var(--accent)}
|
| 44 |
-
.test-btn{padding:8px 18px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif;transition:.2s}
|
| 45 |
-
.test-btn:hover{border-color:var(--accent);color:var(--accent)}
|
| 46 |
-
/* DROP ZONE */
|
| 47 |
.drop-zone{border:2px dashed var(--border);border-radius:20px;background:var(--surface);padding:60px 40px;text-align:center;cursor:pointer;transition:.3s;position:relative}
|
| 48 |
.drop-zone:hover,.drop-zone.over{border-color:var(--accent);background:rgba(124,92,252,0.05)}
|
| 49 |
.drop-icon{width:60px;height:60px;border-radius:16px;background:var(--surface2);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;margin:0 auto 16px;font-size:26px}
|
|
@@ -54,13 +40,10 @@ nav{display:flex;align-items:center;justify-content:space-between;padding:16px 3
|
|
| 54 |
.upload-btn{padding:11px 26px;background:var(--accent);color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:500;cursor:pointer;font-family:'DM Sans',sans-serif;transition:.2s}
|
| 55 |
.upload-btn:hover{background:#6b4de8;transform:translateY(-1px)}
|
| 56 |
#fileInput{display:none}
|
| 57 |
-
/* EDITOR */
|
| 58 |
.editor{display:none}
|
| 59 |
.editor.show{display:block}
|
| 60 |
-
/* MAIN LAYOUT */
|
| 61 |
.main-grid{display:grid;grid-template-columns:1fr 320px;gap:20px;margin-bottom:20px}
|
| 62 |
@media(max-width:780px){.main-grid{grid-template-columns:1fr}}
|
| 63 |
-
/* PANELS */
|
| 64 |
.panel{background:var(--surface);border:1px solid var(--border);border-radius:16px;overflow:hidden}
|
| 65 |
.panel-hd{padding:11px 16px;background:var(--surface2);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
| 66 |
.panel-title{font-size:13px;font-weight:500;color:var(--muted);display:flex;align-items:center;gap:7px}
|
|
@@ -71,11 +54,9 @@ nav{display:flex;align-items:center;justify-content:space-between;padding:16px 3
|
|
| 71 |
.panel-body{min-height:300px;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;padding:12px}
|
| 72 |
.panel-body.checker{background:var(--grid)}
|
| 73 |
.panel-body img{max-width:100%;max-height:380px;object-fit:contain;display:block;margin:auto}
|
| 74 |
-
/* SIDEBAR */
|
| 75 |
.sidebar{display:flex;flex-direction:column;gap:14px}
|
| 76 |
.ctrl-card{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:16px}
|
| 77 |
.ctrl-label{font-size:12px;font-weight:500;color:var(--muted);margin-bottom:10px;display:flex;align-items:center;gap:6px}
|
| 78 |
-
/* MODEL SELECT */
|
| 79 |
.model-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
| 80 |
.model-btn{padding:10px 6px;border-radius:10px;border:1px solid var(--border);background:var(--surface2);cursor:pointer;text-align:center;transition:.2s;font-family:'DM Sans',sans-serif}
|
| 81 |
.model-btn:hover{border-color:var(--accent)}
|
|
@@ -83,7 +64,6 @@ nav{display:flex;align-items:center;justify-content:space-between;padding:16px 3
|
|
| 83 |
.model-btn .mb-icon{font-size:18px;display:block;margin-bottom:3px}
|
| 84 |
.model-btn .mb-name{font-size:12px;font-weight:500;color:var(--text)}
|
| 85 |
.model-btn .mb-desc{font-size:10px;color:var(--muted);margin-top:1px}
|
| 86 |
-
/* BG COLOR */
|
| 87 |
.swatch-row{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:8px}
|
| 88 |
.swatch{width:30px;height:30px;border-radius:7px;border:2px solid transparent;cursor:pointer;transition:.15s;flex-shrink:0}
|
| 89 |
.swatch:hover,.swatch.sel{border-color:var(--accent);transform:scale(1.1)}
|
|
@@ -91,11 +71,9 @@ nav{display:flex;align-items:center;justify-content:space-between;padding:16px 3
|
|
| 91 |
.color-picker{width:34px;height:34px;border-radius:7px;border:1px solid var(--border);padding:2px;background:var(--surface2);cursor:pointer}
|
| 92 |
.hex-input{flex:1;background:var(--surface2);border:1px solid var(--border);border-radius:7px;padding:6px 10px;color:var(--text);font-size:13px;font-family:'DM Sans',sans-serif}
|
| 93 |
.hex-input:focus{outline:none;border-color:var(--accent)}
|
| 94 |
-
/* SLIDERS */
|
| 95 |
.sl-row{margin-bottom:8px}
|
| 96 |
.sl-hd{display:flex;justify-content:space-between;font-size:12px;color:var(--muted);margin-bottom:4px}
|
| 97 |
input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
| 98 |
-
/* TOGGLE */
|
| 99 |
.tog-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
|
| 100 |
.tog-row:last-child{margin-bottom:0}
|
| 101 |
.tog-label{font-size:13px;color:var(--muted)}
|
|
@@ -105,24 +83,19 @@ input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
|
| 105 |
.tog-sl::before{content:'';position:absolute;width:15px;height:15px;border-radius:50%;background:#fff;left:3px;top:3px;transition:.3s}
|
| 106 |
.toggle input:checked+.tog-sl{background:var(--accent)}
|
| 107 |
.toggle input:checked+.tog-sl::before{left:20px}
|
| 108 |
-
/* PROCESS BTN */
|
| 109 |
.process-btn{width:100%;padding:14px;background:var(--accent);border:none;border-radius:12px;color:#fff;font-size:15px;font-weight:600;cursor:pointer;font-family:'Syne',sans-serif;transition:.2s;display:flex;align-items:center;justify-content:center;gap:8px;letter-spacing:.3px}
|
| 110 |
.process-btn:hover{background:#6b4de8;transform:translateY(-1px)}
|
| 111 |
.process-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}
|
| 112 |
-
/* PROGRESS */
|
| 113 |
.progress-wrap{background:var(--surface2);border-radius:50px;height:6px;margin-top:10px;overflow:hidden;display:none}
|
| 114 |
.progress-bar{height:100%;background:linear-gradient(90deg,var(--accent),var(--accent2));border-radius:50px;width:0;transition:.3s}
|
| 115 |
-
/* OVERLAY */
|
| 116 |
.proc-overlay{position:absolute;inset:0;background:rgba(10,10,15,0.88);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;backdrop-filter:blur(6px);display:none}
|
| 117 |
.spinner{width:38px;height:38px;border-radius:50%;border:3px solid var(--border);border-top-color:var(--accent);animation:spin .75s linear infinite}
|
| 118 |
@keyframes spin{to{transform:rotate(360deg)}}
|
| 119 |
.proc-msg{font-size:14px;color:var(--muted)}
|
| 120 |
.proc-sub{font-size:12px;color:var(--border)}
|
| 121 |
-
/* STATS */
|
| 122 |
.stats-row{display:flex;gap:16px;flex-wrap:wrap;padding:10px 0;border-top:1px solid var(--border);margin-top:8px}
|
| 123 |
.stat{font-size:12px;color:var(--muted);display:flex;align-items:center;gap:4px}
|
| 124 |
.stat strong{color:var(--text)}
|
| 125 |
-
/* ACTION BAR */
|
| 126 |
.action-bar{display:flex;gap:10px;flex-wrap:wrap;align-items:center;background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:12px 16px}
|
| 127 |
.act-btn{padding:9px 18px;border-radius:9px;border:1px solid var(--border);background:var(--surface2);color:var(--text);font-size:13px;font-weight:500;cursor:pointer;font-family:'DM Sans',sans-serif;transition:.2s;display:flex;align-items:center;gap:6px}
|
| 128 |
.act-btn:hover{border-color:var(--accent);color:var(--accent)}
|
|
@@ -131,7 +104,6 @@ input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
|
| 131 |
.act-btn:disabled{opacity:.3;cursor:not-allowed}
|
| 132 |
.sel-fmt{padding:8px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:9px;color:var(--text);font-size:13px;font-family:'DM Sans',sans-serif;cursor:pointer}
|
| 133 |
.sel-fmt:focus{outline:none;border-color:var(--accent)}
|
| 134 |
-
/* COMPARE */
|
| 135 |
.cmp-wrap{position:relative;width:100%;cursor:ew-resize;user-select:none;display:inline-block}
|
| 136 |
.cmp-wrap img{display:block;width:100%;max-height:380px;object-fit:contain}
|
| 137 |
.cmp-after{position:absolute;inset:0;overflow:hidden}
|
|
@@ -140,20 +112,16 @@ input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
|
| 140 |
.cmp-handle{position:absolute;top:50%;transform:translate(-50%,-50%);width:30px;height:30px;border-radius:50%;background:var(--accent);display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px;font-weight:700}
|
| 141 |
.cmp-labels{position:absolute;bottom:8px;left:0;right:0;display:flex;justify-content:space-between;padding:0 8px;pointer-events:none}
|
| 142 |
.clabel{padding:2px 9px;border-radius:5px;font-size:11px;background:rgba(0,0,0,0.6);color:#fff}
|
| 143 |
-
/* TABS */
|
| 144 |
.tab-row{display:flex;gap:4px;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:4px;margin-bottom:16px;width:fit-content}
|
| 145 |
.tab{padding:7px 18px;border-radius:8px;border:none;background:transparent;color:var(--muted);font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif;font-weight:500;transition:.2s}
|
| 146 |
.tab.active{background:var(--accent);color:#fff}
|
| 147 |
-
/* HISTORY */
|
| 148 |
.hist-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:10px;margin-top:12px}
|
| 149 |
.hist-item{border-radius:10px;overflow:hidden;border:1px solid var(--border);cursor:pointer;transition:.2s;background:var(--grid);position:relative}
|
| 150 |
.hist-item:hover{border-color:var(--accent);transform:translateY(-2px)}
|
| 151 |
.hist-item img{width:100%;height:80px;object-fit:contain;display:block}
|
| 152 |
.hist-item .hl{padding:4px 7px;font-size:10px;color:var(--muted);background:var(--surface2);border-top:1px solid var(--border);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
| 153 |
-
/* PLACEHOLDER */
|
| 154 |
.ph{color:var(--muted);font-size:13px;text-align:center;padding:30px}
|
| 155 |
.ph-icon{font-size:28px;margin-bottom:8px}
|
| 156 |
-
/* TOAST */
|
| 157 |
#toast{position:fixed;bottom:24px;right:24px;padding:11px 18px;background:var(--surface2);border:1px solid var(--border);border-radius:11px;font-size:13px;color:var(--text);transform:translateY(80px);opacity:0;transition:.3s;z-index:999;display:flex;align-items:center;gap:7px;max-width:320px}
|
| 158 |
#toast.show{transform:translateY(0);opacity:1}
|
| 159 |
</style>
|
|
@@ -175,25 +143,13 @@ input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
|
| 175 |
<span class="tech-pill active">⚡ rembg AI</span>
|
| 176 |
<span class="tech-pill">🐍 Flask Backend</span>
|
| 177 |
<span class="tech-pill">4 AI Models</span>
|
| 178 |
-
<span class="tech-pill">100%
|
| 179 |
</div>
|
| 180 |
-
<h1>
|
| 181 |
-
<p>Professional-grade background removal
|
| 182 |
</div>
|
| 183 |
|
| 184 |
<div class="workspace">
|
| 185 |
-
|
| 186 |
-
<!-- SERVER CONFIG -->
|
| 187 |
-
<div class="server-banner">
|
| 188 |
-
<div class="sb-left">
|
| 189 |
-
<span class="sb-icon">🖥</span>
|
| 190 |
-
<div class="sb-text"><strong>Flask Server URL</strong> — point to your running backend</div>
|
| 191 |
-
</div>
|
| 192 |
-
<input class="server-url" id="serverUrl" type="text" value="http://localhost:5000" placeholder="http://localhost:5000"/>
|
| 193 |
-
<button class="test-btn" onclick="testServer()">🔌 Test Connection</button>
|
| 194 |
-
</div>
|
| 195 |
-
|
| 196 |
-
<!-- UPLOAD -->
|
| 197 |
<div id="uploadSection">
|
| 198 |
<div class="drop-zone" id="dropZone" onclick="document.getElementById('fileInput').click()" ondragover="dov(event)" ondragleave="dol(event)" ondrop="dod(event)">
|
| 199 |
<div class="drop-icon">📸</div>
|
|
@@ -208,9 +164,7 @@ input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
|
| 208 |
<input type="file" id="fileInput" accept="image/*" onchange="loadFile(this.files[0])"/>
|
| 209 |
</div>
|
| 210 |
|
| 211 |
-
<!-- EDITOR -->
|
| 212 |
<div class="editor" id="editor">
|
| 213 |
-
|
| 214 |
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;flex-wrap:wrap;gap:10px">
|
| 215 |
<div class="tab-row">
|
| 216 |
<button class="tab active" id="t-edit" onclick="sv('edit')">✏️ Edit</button>
|
|
@@ -219,10 +173,8 @@ input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
|
| 219 |
<button class="act-btn" onclick="resetAll()" style="font-size:12px;padding:7px 14px">← New Image</button>
|
| 220 |
</div>
|
| 221 |
|
| 222 |
-
<!-- EDIT VIEW -->
|
| 223 |
<div id="v-edit">
|
| 224 |
<div class="main-grid">
|
| 225 |
-
<!-- CANVAS COLUMN -->
|
| 226 |
<div style="display:flex;flex-direction:column;gap:14px">
|
| 227 |
<div class="panel">
|
| 228 |
<div class="panel-hd">
|
|
@@ -247,18 +199,15 @@ input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
|
| 247 |
</div>
|
| 248 |
</div>
|
| 249 |
</div>
|
| 250 |
-
<!-- STATS -->
|
| 251 |
<div class="stats-row" id="statsRow" style="display:none">
|
| 252 |
<div class="stat">📐 <strong id="sDims">—</strong></div>
|
| 253 |
<div class="stat">⏱ <strong id="sTime">—</strong>ms</div>
|
| 254 |
<div class="stat">🤖 Model: <strong id="sModel">—</strong></div>
|
| 255 |
-
<div class="stat" style="color:var(--success)" id="sDone"
|
| 256 |
</div>
|
| 257 |
</div>
|
| 258 |
|
| 259 |
-
<!-- SIDEBAR -->
|
| 260 |
<div class="sidebar">
|
| 261 |
-
<!-- MODEL -->
|
| 262 |
<div class="ctrl-card">
|
| 263 |
<div class="ctrl-label">🤖 AI Model</div>
|
| 264 |
<div class="model-grid">
|
|
@@ -285,18 +234,17 @@ input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
|
| 285 |
</div>
|
| 286 |
</div>
|
| 287 |
|
| 288 |
-
<!-- BACKGROUND -->
|
| 289 |
<div class="ctrl-card">
|
| 290 |
<div class="ctrl-label">���� Background</div>
|
| 291 |
<div class="swatch-row">
|
| 292 |
-
<div class="swatch sel"
|
| 293 |
-
<div class="swatch"
|
| 294 |
-
<div class="swatch"
|
| 295 |
-
<div class="swatch"
|
| 296 |
-
<div class="swatch"
|
| 297 |
-
<div class="swatch"
|
| 298 |
-
<div class="swatch"
|
| 299 |
-
<div class="swatch"
|
| 300 |
</div>
|
| 301 |
<div class="color-row">
|
| 302 |
<input type="color" class="color-picker" id="cpicker" value="#7c5cfc" oninput="selSwatch(this.value,null)"/>
|
|
@@ -304,7 +252,6 @@ input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
|
| 304 |
</div>
|
| 305 |
</div>
|
| 306 |
|
| 307 |
-
<!-- REFINEMENT -->
|
| 308 |
<div class="ctrl-card">
|
| 309 |
<div class="ctrl-label">🔧 Edge Refinement</div>
|
| 310 |
<div class="sl-row">
|
|
@@ -313,7 +260,6 @@ input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
|
| 313 |
</div>
|
| 314 |
</div>
|
| 315 |
|
| 316 |
-
<!-- SHADOW -->
|
| 317 |
<div class="ctrl-card">
|
| 318 |
<div class="ctrl-label">✨ Effects</div>
|
| 319 |
<div class="tog-row">
|
|
@@ -332,7 +278,6 @@ input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
|
| 332 |
</div>
|
| 333 |
</div>
|
| 334 |
|
| 335 |
-
<!-- OUTPUT -->
|
| 336 |
<div class="ctrl-card">
|
| 337 |
<div class="ctrl-label">📤 Output</div>
|
| 338 |
<div style="display:flex;gap:8px;margin-bottom:10px">
|
|
@@ -349,7 +294,6 @@ input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
|
| 349 |
</div>
|
| 350 |
</div>
|
| 351 |
|
| 352 |
-
<!-- PROCESS -->
|
| 353 |
<button class="process-btn" id="procBtn" onclick="processImage()">
|
| 354 |
<span id="procIcon">✨</span> <span id="procTxt">Remove Background</span>
|
| 355 |
</button>
|
|
@@ -359,7 +303,6 @@ input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
|
| 359 |
</div>
|
| 360 |
</div>
|
| 361 |
|
| 362 |
-
<!-- ACTION BAR -->
|
| 363 |
<div class="action-bar">
|
| 364 |
<button class="act-btn" onclick="copyImg()" id="copyBtn" disabled>📋 Copy PNG</button>
|
| 365 |
<div style="flex:1"></div>
|
|
@@ -367,14 +310,12 @@ input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
|
| 367 |
<button class="act-btn dl" onclick="dlResult()" id="dlBtn" disabled>⬇ Download</button>
|
| 368 |
</div>
|
| 369 |
|
| 370 |
-
<!-- HISTORY -->
|
| 371 |
<div id="histSection" style="margin-top:24px;display:none">
|
| 372 |
<div style="font-family:'Syne',sans-serif;font-weight:700;font-size:15px;color:var(--muted);margin-bottom:4px">History <span id="histCount" style="font-weight:400;font-size:13px"></span></div>
|
| 373 |
<div class="hist-grid" id="histGrid"></div>
|
| 374 |
</div>
|
| 375 |
</div>
|
| 376 |
|
| 377 |
-
<!-- COMPARE VIEW -->
|
| 378 |
<div id="v-compare" style="display:none">
|
| 379 |
<div class="panel">
|
| 380 |
<div class="panel-hd"><span class="panel-title">⚡ Before / After — drag to compare</span></div>
|
|
@@ -389,7 +330,12 @@ input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
|
| 389 |
<div id="toast"><span id="tIcon"></span><span id="tMsg"></span></div>
|
| 390 |
|
| 391 |
<script>
|
| 392 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
let origFile = null, origDataUrl = null;
|
| 394 |
let resultBlob = null, resultUrl = null;
|
| 395 |
let selModel = 'general';
|
|
@@ -397,33 +343,28 @@ let selBg = 'transparent';
|
|
| 397 |
let history = [];
|
| 398 |
let curView = 'edit';
|
| 399 |
|
| 400 |
-
// ── SERVER ──────────────────────────────────────────────
|
| 401 |
-
function getServer(){ return document.getElementById('serverUrl').value.replace(/\/$/,''); }
|
| 402 |
-
|
| 403 |
async function testServer(){
|
| 404 |
const url = getServer();
|
| 405 |
const dot = document.getElementById('statusDot');
|
| 406 |
const txt = document.getElementById('statusText');
|
| 407 |
-
dot.className = 'status-dot'; txt.textContent = '
|
| 408 |
try {
|
| 409 |
-
const r = await fetch(url+'/health', {signal: AbortSignal.timeout(
|
| 410 |
const d = await r.json();
|
| 411 |
dot.className = 'status-dot online';
|
| 412 |
txt.textContent = 'Server online ✓';
|
| 413 |
-
toast('✓','
|
| 414 |
} catch(e){
|
| 415 |
dot.className = 'status-dot offline';
|
| 416 |
txt.textContent = 'Server offline';
|
| 417 |
-
toast('⚠','Cannot reach server
|
| 418 |
}
|
| 419 |
}
|
| 420 |
|
| 421 |
-
// ── DRAG & DROP ─────────────────────────────────────────
|
| 422 |
function dov(e){ e.preventDefault(); document.getElementById('dropZone').classList.add('over'); }
|
| 423 |
function dol(){ document.getElementById('dropZone').classList.remove('over'); }
|
| 424 |
function dod(e){ e.preventDefault(); dol(); const f=e.dataTransfer.files[0]; if(f&&f.type.startsWith('image/')) loadFile(f); }
|
| 425 |
|
| 426 |
-
// ── LOAD ────────────────────────────────────────────────
|
| 427 |
function loadFile(file){
|
| 428 |
if(!file) return;
|
| 429 |
origFile = file;
|
|
@@ -432,11 +373,9 @@ function loadFile(file){
|
|
| 432 |
origDataUrl = e.target.result;
|
| 433 |
document.getElementById('uploadSection').style.display='none';
|
| 434 |
document.getElementById('editor').classList.add('show');
|
| 435 |
-
// show original
|
| 436 |
const op = document.getElementById('origPanel');
|
| 437 |
op.innerHTML = `<img src="${origDataUrl}"/>`;
|
| 438 |
document.getElementById('imgMeta').textContent = file.name+' · '+(file.size<1048576?(file.size/1024).toFixed(1)+'KB':(file.size/1048576).toFixed(2)+'MB');
|
| 439 |
-
// reset result
|
| 440 |
document.getElementById('resPanel').innerHTML = `<div class="ph" id="resPlaceholder"><div class="ph-icon">✨</div>Click "Remove Background"</div><div class="proc-overlay" id="procOverlay"><div class="spinner"></div><div class="proc-msg">Processing with rembg…</div><div class="proc-sub" id="procSub">Loading model</div></div>`;
|
| 441 |
resultBlob=null; resultUrl=null;
|
| 442 |
document.getElementById('dlBtn').disabled=true;
|
|
@@ -452,7 +391,6 @@ function resetAll(){
|
|
| 452 |
document.getElementById('editor').classList.remove('show');
|
| 453 |
}
|
| 454 |
|
| 455 |
-
// ── PROCESS ─────────────────────────────────────────────
|
| 456 |
async function processImage(){
|
| 457 |
if(!origFile) return toast('⚠','Upload an image first','error');
|
| 458 |
const url = getServer();
|
|
@@ -462,13 +400,12 @@ async function processImage(){
|
|
| 462 |
document.getElementById('procIcon').textContent='⏳';
|
| 463 |
document.getElementById('procTxt').textContent='Sending to rembg…';
|
| 464 |
overlay.style.display='flex';
|
| 465 |
-
|
|
|
|
| 466 |
|
| 467 |
-
// Progress pulse
|
| 468 |
const pw = document.getElementById('progWrap'); pw.style.display='block';
|
| 469 |
const pb = document.getElementById('progBar');
|
| 470 |
let prog=0, pInterval = setInterval(()=>{ prog=Math.min(prog+Math.random()*8,88); pb.style.width=prog+'%'; },300);
|
| 471 |
-
|
| 472 |
const msgs=['Loading AI model…','Detecting edges…','Separating subject…','Refining mask…','Compositing result…'];
|
| 473 |
let mi=0, mInterval = setInterval(()=>{ const el=document.getElementById('procSub'); if(el) el.textContent=msgs[mi++%msgs.length]; },1500);
|
| 474 |
|
|
@@ -494,7 +431,6 @@ async function processImage(){
|
|
| 494 |
|
| 495 |
resultBlob = blob;
|
| 496 |
resultUrl = URL.createObjectURL(blob);
|
| 497 |
-
|
| 498 |
const elapsed = resp.headers.get('X-Processing-Time') || (Date.now()-t0);
|
| 499 |
const w = resp.headers.get('X-Image-Width')||'—';
|
| 500 |
const h = resp.headers.get('X-Image-Height')||'—';
|
|
@@ -510,14 +446,12 @@ async function processImage(){
|
|
| 510 |
document.getElementById('sDims').textContent=w+'×'+h;
|
| 511 |
document.getElementById('sTime').textContent=elapsed;
|
| 512 |
document.getElementById('sModel').textContent=selModel;
|
| 513 |
-
document.getElementById('sDone').style.display='flex';
|
| 514 |
document.getElementById('resMeta').textContent=w+'×'+h+' '+document.getElementById('outFmt').value.toUpperCase();
|
| 515 |
-
|
| 516 |
document.getElementById('dlBtn').disabled=false;
|
| 517 |
document.getElementById('copyBtn').disabled=false;
|
| 518 |
addHistory(resultUrl, origFile.name);
|
| 519 |
if(curView==='compare') buildCompare();
|
| 520 |
-
toast('✓','Background removed
|
| 521 |
} catch(e){
|
| 522 |
clearInterval(pInterval); clearInterval(mInterval);
|
| 523 |
pw.style.display='none';
|
|
@@ -531,7 +465,6 @@ async function processImage(){
|
|
| 531 |
document.getElementById('procTxt').textContent='Remove Background';
|
| 532 |
}
|
| 533 |
|
| 534 |
-
// ── CONTROLS ────────────────────────────────────────────
|
| 535 |
function setModel(m,el){
|
| 536 |
selModel=m;
|
| 537 |
document.querySelectorAll('.model-btn').forEach(b=>b.classList.remove('active'));
|
|
@@ -544,19 +477,14 @@ function selSwatch(c,el){
|
|
| 544 |
document.getElementById('hexIn').value = c==='transparent'?'':c;
|
| 545 |
if(c!=='transparent'&&c.startsWith('#')) document.getElementById('cpicker').value=c;
|
| 546 |
}
|
| 547 |
-
function onHex(v){
|
| 548 |
-
if(/^#[0-9a-fA-F]{6}$/.test(v)) selSwatch(v,null);
|
| 549 |
-
}
|
| 550 |
function toggleShadowOpts(on){ document.getElementById('shadowOpts').style.display=on?'block':'none'; }
|
| 551 |
|
| 552 |
-
// ── DOWNLOAD ────────────────────────────────────────────
|
| 553 |
function dlResult(){
|
| 554 |
if(!resultBlob) return;
|
| 555 |
const fmt = document.getElementById('outFmt').value;
|
| 556 |
const a = document.createElement('a');
|
| 557 |
-
a.href = resultUrl;
|
| 558 |
-
a.download = 'cutout.'+fmt;
|
| 559 |
-
a.click();
|
| 560 |
toast('⬇','Downloaded!','success');
|
| 561 |
}
|
| 562 |
|
|
@@ -565,20 +493,17 @@ async function copyImg(){
|
|
| 565 |
try {
|
| 566 |
const png = resultBlob.type==='image/png' ? resultBlob : await convertToPng(resultBlob);
|
| 567 |
await navigator.clipboard.write([new ClipboardItem({'image/png':png})]);
|
| 568 |
-
toast('📋','Copied
|
| 569 |
} catch(e){ toast('⚠','Copy failed: '+e.message,'error'); }
|
| 570 |
}
|
| 571 |
-
|
| 572 |
async function convertToPng(blob){
|
| 573 |
const url=URL.createObjectURL(blob);
|
| 574 |
-
const img=await
|
| 575 |
const c=document.createElement('canvas'); c.width=img.width; c.height=img.height;
|
| 576 |
c.getContext('2d').drawImage(img,0,0);
|
| 577 |
return new Promise(res=>c.toBlob(res,'image/png'));
|
| 578 |
}
|
| 579 |
-
function loadImg(src){ return new Promise((res,rej)=>{ const i=new Image(); i.onload=()=>res(i); i.onerror=rej; i.src=src; }); }
|
| 580 |
|
| 581 |
-
// ── COMPARE ─────────────────────────────────────────────
|
| 582 |
function sv(v){
|
| 583 |
curView=v;
|
| 584 |
['edit','compare'].forEach(x=>{ document.getElementById('v-'+x).style.display=x===v?'block':'none'; document.getElementById('t-'+x).classList.toggle('active',x===v); });
|
|
@@ -589,23 +514,14 @@ function buildCompare(){
|
|
| 589 |
const c=document.getElementById('cmpContainer');
|
| 590 |
if(!resultUrl||!origDataUrl){ c.innerHTML='<div class="ph"><div class="ph-icon">⚡</div>Process an image first</div>'; return; }
|
| 591 |
c.innerHTML=`<div class="cmp-wrap" id="cw" style="max-width:640px;width:100%"><img src="${origDataUrl}" id="cb"/><div class="cmp-after" id="ca" style="clip-path:inset(0 50% 0 0)"><img src="${resultUrl}"/></div><div class="cmp-div" id="cd" style="left:50%"><div class="cmp-handle">↔</div></div><div class="cmp-labels"><span class="clabel">Original</span><span class="clabel">Result</span></div></div>`;
|
| 592 |
-
const wrap=document.getElementById('cw'),
|
| 593 |
let drag=false;
|
| 594 |
wrap.addEventListener('mousedown',()=>drag=true);
|
| 595 |
document.addEventListener('mouseup',()=>drag=false);
|
| 596 |
-
document.addEventListener('mousemove',e=>{
|
| 597 |
-
|
| 598 |
-
const p=Math.max(0,Math.min(100,((e.clientX-r.left)/r.width)*100));
|
| 599 |
-
div.style.left=p+'%'; after.style.clipPath=`inset(0 ${100-p}% 0 0)`;
|
| 600 |
-
});
|
| 601 |
-
wrap.addEventListener('touchmove',e=>{
|
| 602 |
-
const r=wrap.getBoundingClientRect();
|
| 603 |
-
const p=Math.max(0,Math.min(100,((e.touches[0].clientX-r.left)/r.width)*100));
|
| 604 |
-
div.style.left=p+'%'; after.style.clipPath=`inset(0 ${100-p}% 0 0)`;
|
| 605 |
-
},{passive:true});
|
| 606 |
}
|
| 607 |
|
| 608 |
-
// ── HISTORY ────────────────────────────��────────────────
|
| 609 |
function addHistory(url,name){
|
| 610 |
history.unshift({url,name});
|
| 611 |
if(history.length>16) history.pop();
|
|
@@ -623,7 +539,6 @@ function restoreHist(i){
|
|
| 623 |
toast('↩','Restored from history','success');
|
| 624 |
}
|
| 625 |
|
| 626 |
-
// ── TOAST ───────────────────────────────────────────────
|
| 627 |
function toast(icon,msg,type=''){
|
| 628 |
const t=document.getElementById('toast');
|
| 629 |
document.getElementById('tIcon').textContent=icon;
|
|
@@ -632,7 +547,6 @@ function toast(icon,msg,type=''){
|
|
| 632 |
t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),3500);
|
| 633 |
}
|
| 634 |
|
| 635 |
-
// ── INIT ────────────────────────────────────────────────
|
| 636 |
testServer();
|
| 637 |
</script>
|
| 638 |
</body>
|
|
|
|
| 14 |
--grid:repeating-conic-gradient(#1c1c2880 0% 25%,#13131a 0% 50%) 0 0/20px 20px;
|
| 15 |
}
|
| 16 |
body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden}
|
|
|
|
| 17 |
nav{display:flex;align-items:center;justify-content:space-between;padding:16px 36px;border-bottom:1px solid var(--border);position:sticky;top:0;background:rgba(10,10,15,0.9);backdrop-filter:blur(20px);z-index:100}
|
| 18 |
.logo{font-family:'Syne',sans-serif;font-weight:800;font-size:21px;display:flex;align-items:center;gap:10px}
|
| 19 |
.logo-badge{background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
|
|
|
| 21 |
.status-dot{width:7px;height:7px;border-radius:50%;background:var(--border);transition:.3s}
|
| 22 |
.status-dot.online{background:var(--success);box-shadow:0 0 6px var(--success)}
|
| 23 |
.status-dot.offline{background:var(--danger)}
|
|
|
|
| 24 |
.hero{text-align:center;padding:48px 20px 36px;position:relative}
|
| 25 |
.hero::before{content:'';position:absolute;width:500px;height:500px;border-radius:50%;background:radial-gradient(circle,rgba(124,92,252,0.1) 0%,transparent 70%);top:50%;left:50%;transform:translate(-50%,-50%);pointer-events:none}
|
| 26 |
.hero h1{font-family:'Syne',sans-serif;font-size:clamp(32px,5.5vw,64px);font-weight:800;line-height:1.05;letter-spacing:-2px;margin-bottom:12px}
|
|
|
|
| 29 |
.tech-row{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}
|
| 30 |
.tech-pill{padding:5px 14px;border-radius:50px;border:1px solid var(--border);background:var(--surface);color:var(--muted);font-size:12px;font-weight:500}
|
| 31 |
.tech-pill.active{border-color:var(--accent);color:var(--accent);background:rgba(124,92,252,0.08)}
|
|
|
|
| 32 |
.workspace{max-width:1080px;margin:0 auto;padding:0 20px 60px}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
.drop-zone{border:2px dashed var(--border);border-radius:20px;background:var(--surface);padding:60px 40px;text-align:center;cursor:pointer;transition:.3s;position:relative}
|
| 34 |
.drop-zone:hover,.drop-zone.over{border-color:var(--accent);background:rgba(124,92,252,0.05)}
|
| 35 |
.drop-icon{width:60px;height:60px;border-radius:16px;background:var(--surface2);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;margin:0 auto 16px;font-size:26px}
|
|
|
|
| 40 |
.upload-btn{padding:11px 26px;background:var(--accent);color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:500;cursor:pointer;font-family:'DM Sans',sans-serif;transition:.2s}
|
| 41 |
.upload-btn:hover{background:#6b4de8;transform:translateY(-1px)}
|
| 42 |
#fileInput{display:none}
|
|
|
|
| 43 |
.editor{display:none}
|
| 44 |
.editor.show{display:block}
|
|
|
|
| 45 |
.main-grid{display:grid;grid-template-columns:1fr 320px;gap:20px;margin-bottom:20px}
|
| 46 |
@media(max-width:780px){.main-grid{grid-template-columns:1fr}}
|
|
|
|
| 47 |
.panel{background:var(--surface);border:1px solid var(--border);border-radius:16px;overflow:hidden}
|
| 48 |
.panel-hd{padding:11px 16px;background:var(--surface2);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
| 49 |
.panel-title{font-size:13px;font-weight:500;color:var(--muted);display:flex;align-items:center;gap:7px}
|
|
|
|
| 54 |
.panel-body{min-height:300px;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;padding:12px}
|
| 55 |
.panel-body.checker{background:var(--grid)}
|
| 56 |
.panel-body img{max-width:100%;max-height:380px;object-fit:contain;display:block;margin:auto}
|
|
|
|
| 57 |
.sidebar{display:flex;flex-direction:column;gap:14px}
|
| 58 |
.ctrl-card{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:16px}
|
| 59 |
.ctrl-label{font-size:12px;font-weight:500;color:var(--muted);margin-bottom:10px;display:flex;align-items:center;gap:6px}
|
|
|
|
| 60 |
.model-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
| 61 |
.model-btn{padding:10px 6px;border-radius:10px;border:1px solid var(--border);background:var(--surface2);cursor:pointer;text-align:center;transition:.2s;font-family:'DM Sans',sans-serif}
|
| 62 |
.model-btn:hover{border-color:var(--accent)}
|
|
|
|
| 64 |
.model-btn .mb-icon{font-size:18px;display:block;margin-bottom:3px}
|
| 65 |
.model-btn .mb-name{font-size:12px;font-weight:500;color:var(--text)}
|
| 66 |
.model-btn .mb-desc{font-size:10px;color:var(--muted);margin-top:1px}
|
|
|
|
| 67 |
.swatch-row{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:8px}
|
| 68 |
.swatch{width:30px;height:30px;border-radius:7px;border:2px solid transparent;cursor:pointer;transition:.15s;flex-shrink:0}
|
| 69 |
.swatch:hover,.swatch.sel{border-color:var(--accent);transform:scale(1.1)}
|
|
|
|
| 71 |
.color-picker{width:34px;height:34px;border-radius:7px;border:1px solid var(--border);padding:2px;background:var(--surface2);cursor:pointer}
|
| 72 |
.hex-input{flex:1;background:var(--surface2);border:1px solid var(--border);border-radius:7px;padding:6px 10px;color:var(--text);font-size:13px;font-family:'DM Sans',sans-serif}
|
| 73 |
.hex-input:focus{outline:none;border-color:var(--accent)}
|
|
|
|
| 74 |
.sl-row{margin-bottom:8px}
|
| 75 |
.sl-hd{display:flex;justify-content:space-between;font-size:12px;color:var(--muted);margin-bottom:4px}
|
| 76 |
input[type=range]{width:100%;accent-color:var(--accent);height:3px}
|
|
|
|
| 77 |
.tog-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
|
| 78 |
.tog-row:last-child{margin-bottom:0}
|
| 79 |
.tog-label{font-size:13px;color:var(--muted)}
|
|
|
|
| 83 |
.tog-sl::before{content:'';position:absolute;width:15px;height:15px;border-radius:50%;background:#fff;left:3px;top:3px;transition:.3s}
|
| 84 |
.toggle input:checked+.tog-sl{background:var(--accent)}
|
| 85 |
.toggle input:checked+.tog-sl::before{left:20px}
|
|
|
|
| 86 |
.process-btn{width:100%;padding:14px;background:var(--accent);border:none;border-radius:12px;color:#fff;font-size:15px;font-weight:600;cursor:pointer;font-family:'Syne',sans-serif;transition:.2s;display:flex;align-items:center;justify-content:center;gap:8px;letter-spacing:.3px}
|
| 87 |
.process-btn:hover{background:#6b4de8;transform:translateY(-1px)}
|
| 88 |
.process-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}
|
|
|
|
| 89 |
.progress-wrap{background:var(--surface2);border-radius:50px;height:6px;margin-top:10px;overflow:hidden;display:none}
|
| 90 |
.progress-bar{height:100%;background:linear-gradient(90deg,var(--accent),var(--accent2));border-radius:50px;width:0;transition:.3s}
|
|
|
|
| 91 |
.proc-overlay{position:absolute;inset:0;background:rgba(10,10,15,0.88);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;backdrop-filter:blur(6px);display:none}
|
| 92 |
.spinner{width:38px;height:38px;border-radius:50%;border:3px solid var(--border);border-top-color:var(--accent);animation:spin .75s linear infinite}
|
| 93 |
@keyframes spin{to{transform:rotate(360deg)}}
|
| 94 |
.proc-msg{font-size:14px;color:var(--muted)}
|
| 95 |
.proc-sub{font-size:12px;color:var(--border)}
|
|
|
|
| 96 |
.stats-row{display:flex;gap:16px;flex-wrap:wrap;padding:10px 0;border-top:1px solid var(--border);margin-top:8px}
|
| 97 |
.stat{font-size:12px;color:var(--muted);display:flex;align-items:center;gap:4px}
|
| 98 |
.stat strong{color:var(--text)}
|
|
|
|
| 99 |
.action-bar{display:flex;gap:10px;flex-wrap:wrap;align-items:center;background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:12px 16px}
|
| 100 |
.act-btn{padding:9px 18px;border-radius:9px;border:1px solid var(--border);background:var(--surface2);color:var(--text);font-size:13px;font-weight:500;cursor:pointer;font-family:'DM Sans',sans-serif;transition:.2s;display:flex;align-items:center;gap:6px}
|
| 101 |
.act-btn:hover{border-color:var(--accent);color:var(--accent)}
|
|
|
|
| 104 |
.act-btn:disabled{opacity:.3;cursor:not-allowed}
|
| 105 |
.sel-fmt{padding:8px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:9px;color:var(--text);font-size:13px;font-family:'DM Sans',sans-serif;cursor:pointer}
|
| 106 |
.sel-fmt:focus{outline:none;border-color:var(--accent)}
|
|
|
|
| 107 |
.cmp-wrap{position:relative;width:100%;cursor:ew-resize;user-select:none;display:inline-block}
|
| 108 |
.cmp-wrap img{display:block;width:100%;max-height:380px;object-fit:contain}
|
| 109 |
.cmp-after{position:absolute;inset:0;overflow:hidden}
|
|
|
|
| 112 |
.cmp-handle{position:absolute;top:50%;transform:translate(-50%,-50%);width:30px;height:30px;border-radius:50%;background:var(--accent);display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px;font-weight:700}
|
| 113 |
.cmp-labels{position:absolute;bottom:8px;left:0;right:0;display:flex;justify-content:space-between;padding:0 8px;pointer-events:none}
|
| 114 |
.clabel{padding:2px 9px;border-radius:5px;font-size:11px;background:rgba(0,0,0,0.6);color:#fff}
|
|
|
|
| 115 |
.tab-row{display:flex;gap:4px;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:4px;margin-bottom:16px;width:fit-content}
|
| 116 |
.tab{padding:7px 18px;border-radius:8px;border:none;background:transparent;color:var(--muted);font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif;font-weight:500;transition:.2s}
|
| 117 |
.tab.active{background:var(--accent);color:#fff}
|
|
|
|
| 118 |
.hist-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:10px;margin-top:12px}
|
| 119 |
.hist-item{border-radius:10px;overflow:hidden;border:1px solid var(--border);cursor:pointer;transition:.2s;background:var(--grid);position:relative}
|
| 120 |
.hist-item:hover{border-color:var(--accent);transform:translateY(-2px)}
|
| 121 |
.hist-item img{width:100%;height:80px;object-fit:contain;display:block}
|
| 122 |
.hist-item .hl{padding:4px 7px;font-size:10px;color:var(--muted);background:var(--surface2);border-top:1px solid var(--border);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
|
|
| 123 |
.ph{color:var(--muted);font-size:13px;text-align:center;padding:30px}
|
| 124 |
.ph-icon{font-size:28px;margin-bottom:8px}
|
|
|
|
| 125 |
#toast{position:fixed;bottom:24px;right:24px;padding:11px 18px;background:var(--surface2);border:1px solid var(--border);border-radius:11px;font-size:13px;color:var(--text);transform:translateY(80px);opacity:0;transition:.3s;z-index:999;display:flex;align-items:center;gap:7px;max-width:320px}
|
| 126 |
#toast.show{transform:translateY(0);opacity:1}
|
| 127 |
</style>
|
|
|
|
| 143 |
<span class="tech-pill active">⚡ rembg AI</span>
|
| 144 |
<span class="tech-pill">🐍 Flask Backend</span>
|
| 145 |
<span class="tech-pill">4 AI Models</span>
|
| 146 |
+
<span class="tech-pill">100% Free</span>
|
| 147 |
</div>
|
| 148 |
+
<h1>Background Removal<br><span>Powered by rembg</span></h1>
|
| 149 |
+
<p>Professional-grade AI background removal — no limits, no watermarks.</p>
|
| 150 |
</div>
|
| 151 |
|
| 152 |
<div class="workspace">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
<div id="uploadSection">
|
| 154 |
<div class="drop-zone" id="dropZone" onclick="document.getElementById('fileInput').click()" ondragover="dov(event)" ondragleave="dol(event)" ondrop="dod(event)">
|
| 155 |
<div class="drop-icon">📸</div>
|
|
|
|
| 164 |
<input type="file" id="fileInput" accept="image/*" onchange="loadFile(this.files[0])"/>
|
| 165 |
</div>
|
| 166 |
|
|
|
|
| 167 |
<div class="editor" id="editor">
|
|
|
|
| 168 |
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;flex-wrap:wrap;gap:10px">
|
| 169 |
<div class="tab-row">
|
| 170 |
<button class="tab active" id="t-edit" onclick="sv('edit')">✏️ Edit</button>
|
|
|
|
| 173 |
<button class="act-btn" onclick="resetAll()" style="font-size:12px;padding:7px 14px">← New Image</button>
|
| 174 |
</div>
|
| 175 |
|
|
|
|
| 176 |
<div id="v-edit">
|
| 177 |
<div class="main-grid">
|
|
|
|
| 178 |
<div style="display:flex;flex-direction:column;gap:14px">
|
| 179 |
<div class="panel">
|
| 180 |
<div class="panel-hd">
|
|
|
|
| 199 |
</div>
|
| 200 |
</div>
|
| 201 |
</div>
|
|
|
|
| 202 |
<div class="stats-row" id="statsRow" style="display:none">
|
| 203 |
<div class="stat">📐 <strong id="sDims">—</strong></div>
|
| 204 |
<div class="stat">⏱ <strong id="sTime">—</strong>ms</div>
|
| 205 |
<div class="stat">🤖 Model: <strong id="sModel">—</strong></div>
|
| 206 |
+
<div class="stat" style="color:var(--success)" id="sDone">✓ Done!</div>
|
| 207 |
</div>
|
| 208 |
</div>
|
| 209 |
|
|
|
|
| 210 |
<div class="sidebar">
|
|
|
|
| 211 |
<div class="ctrl-card">
|
| 212 |
<div class="ctrl-label">🤖 AI Model</div>
|
| 213 |
<div class="model-grid">
|
|
|
|
| 234 |
</div>
|
| 235 |
</div>
|
| 236 |
|
|
|
|
| 237 |
<div class="ctrl-card">
|
| 238 |
<div class="ctrl-label">���� Background</div>
|
| 239 |
<div class="swatch-row">
|
| 240 |
+
<div class="swatch sel" style="background:repeating-conic-gradient(#888 0% 25%,#ccc 0% 50%) 0 0/12px 12px" onclick="selSwatch('transparent',this)" title="Transparent"></div>
|
| 241 |
+
<div class="swatch" style="background:#fff;border-color:#ddd" onclick="selSwatch('#ffffff',this)"></div>
|
| 242 |
+
<div class="swatch" style="background:#000" onclick="selSwatch('#000000',this)"></div>
|
| 243 |
+
<div class="swatch" style="background:#3b82f6" onclick="selSwatch('#3b82f6',this)"></div>
|
| 244 |
+
<div class="swatch" style="background:#22c55e" onclick="selSwatch('#22c55e',this)"></div>
|
| 245 |
+
<div class="swatch" style="background:#f59e0b" onclick="selSwatch('#f59e0b',this)"></div>
|
| 246 |
+
<div class="swatch" style="background:#ec4899" onclick="selSwatch('#ec4899',this)"></div>
|
| 247 |
+
<div class="swatch" style="background:#8b5cf6" onclick="selSwatch('#8b5cf6',this)"></div>
|
| 248 |
</div>
|
| 249 |
<div class="color-row">
|
| 250 |
<input type="color" class="color-picker" id="cpicker" value="#7c5cfc" oninput="selSwatch(this.value,null)"/>
|
|
|
|
| 252 |
</div>
|
| 253 |
</div>
|
| 254 |
|
|
|
|
| 255 |
<div class="ctrl-card">
|
| 256 |
<div class="ctrl-label">🔧 Edge Refinement</div>
|
| 257 |
<div class="sl-row">
|
|
|
|
| 260 |
</div>
|
| 261 |
</div>
|
| 262 |
|
|
|
|
| 263 |
<div class="ctrl-card">
|
| 264 |
<div class="ctrl-label">✨ Effects</div>
|
| 265 |
<div class="tog-row">
|
|
|
|
| 278 |
</div>
|
| 279 |
</div>
|
| 280 |
|
|
|
|
| 281 |
<div class="ctrl-card">
|
| 282 |
<div class="ctrl-label">📤 Output</div>
|
| 283 |
<div style="display:flex;gap:8px;margin-bottom:10px">
|
|
|
|
| 294 |
</div>
|
| 295 |
</div>
|
| 296 |
|
|
|
|
| 297 |
<button class="process-btn" id="procBtn" onclick="processImage()">
|
| 298 |
<span id="procIcon">✨</span> <span id="procTxt">Remove Background</span>
|
| 299 |
</button>
|
|
|
|
| 303 |
</div>
|
| 304 |
</div>
|
| 305 |
|
|
|
|
| 306 |
<div class="action-bar">
|
| 307 |
<button class="act-btn" onclick="copyImg()" id="copyBtn" disabled>📋 Copy PNG</button>
|
| 308 |
<div style="flex:1"></div>
|
|
|
|
| 310 |
<button class="act-btn dl" onclick="dlResult()" id="dlBtn" disabled>⬇ Download</button>
|
| 311 |
</div>
|
| 312 |
|
|
|
|
| 313 |
<div id="histSection" style="margin-top:24px;display:none">
|
| 314 |
<div style="font-family:'Syne',sans-serif;font-weight:700;font-size:15px;color:var(--muted);margin-bottom:4px">History <span id="histCount" style="font-weight:400;font-size:13px"></span></div>
|
| 315 |
<div class="hist-grid" id="histGrid"></div>
|
| 316 |
</div>
|
| 317 |
</div>
|
| 318 |
|
|
|
|
| 319 |
<div id="v-compare" style="display:none">
|
| 320 |
<div class="panel">
|
| 321 |
<div class="panel-hd"><span class="panel-title">⚡ Before / After — drag to compare</span></div>
|
|
|
|
| 330 |
<div id="toast"><span id="tIcon"></span><span id="tMsg"></span></div>
|
| 331 |
|
| 332 |
<script>
|
| 333 |
+
// ── AUTO-DETECT SERVER URL ───────────────────────────────
|
| 334 |
+
// Works on localhost AND on HuggingFace/Railway/any deployment
|
| 335 |
+
function getServer(){
|
| 336 |
+
return window.location.origin;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
let origFile = null, origDataUrl = null;
|
| 340 |
let resultBlob = null, resultUrl = null;
|
| 341 |
let selModel = 'general';
|
|
|
|
| 343 |
let history = [];
|
| 344 |
let curView = 'edit';
|
| 345 |
|
|
|
|
|
|
|
|
|
|
| 346 |
async function testServer(){
|
| 347 |
const url = getServer();
|
| 348 |
const dot = document.getElementById('statusDot');
|
| 349 |
const txt = document.getElementById('statusText');
|
| 350 |
+
dot.className = 'status-dot'; txt.textContent = 'Checking…';
|
| 351 |
try {
|
| 352 |
+
const r = await fetch(url+'/health', {signal: AbortSignal.timeout(6000)});
|
| 353 |
const d = await r.json();
|
| 354 |
dot.className = 'status-dot online';
|
| 355 |
txt.textContent = 'Server online ✓';
|
| 356 |
+
toast('✓','Connected! Models: '+d.models.join(', '),'success');
|
| 357 |
} catch(e){
|
| 358 |
dot.className = 'status-dot offline';
|
| 359 |
txt.textContent = 'Server offline';
|
| 360 |
+
toast('⚠','Cannot reach server','error');
|
| 361 |
}
|
| 362 |
}
|
| 363 |
|
|
|
|
| 364 |
function dov(e){ e.preventDefault(); document.getElementById('dropZone').classList.add('over'); }
|
| 365 |
function dol(){ document.getElementById('dropZone').classList.remove('over'); }
|
| 366 |
function dod(e){ e.preventDefault(); dol(); const f=e.dataTransfer.files[0]; if(f&&f.type.startsWith('image/')) loadFile(f); }
|
| 367 |
|
|
|
|
| 368 |
function loadFile(file){
|
| 369 |
if(!file) return;
|
| 370 |
origFile = file;
|
|
|
|
| 373 |
origDataUrl = e.target.result;
|
| 374 |
document.getElementById('uploadSection').style.display='none';
|
| 375 |
document.getElementById('editor').classList.add('show');
|
|
|
|
| 376 |
const op = document.getElementById('origPanel');
|
| 377 |
op.innerHTML = `<img src="${origDataUrl}"/>`;
|
| 378 |
document.getElementById('imgMeta').textContent = file.name+' · '+(file.size<1048576?(file.size/1024).toFixed(1)+'KB':(file.size/1048576).toFixed(2)+'MB');
|
|
|
|
| 379 |
document.getElementById('resPanel').innerHTML = `<div class="ph" id="resPlaceholder"><div class="ph-icon">✨</div>Click "Remove Background"</div><div class="proc-overlay" id="procOverlay"><div class="spinner"></div><div class="proc-msg">Processing with rembg…</div><div class="proc-sub" id="procSub">Loading model</div></div>`;
|
| 380 |
resultBlob=null; resultUrl=null;
|
| 381 |
document.getElementById('dlBtn').disabled=true;
|
|
|
|
| 391 |
document.getElementById('editor').classList.remove('show');
|
| 392 |
}
|
| 393 |
|
|
|
|
| 394 |
async function processImage(){
|
| 395 |
if(!origFile) return toast('⚠','Upload an image first','error');
|
| 396 |
const url = getServer();
|
|
|
|
| 400 |
document.getElementById('procIcon').textContent='⏳';
|
| 401 |
document.getElementById('procTxt').textContent='Sending to rembg…';
|
| 402 |
overlay.style.display='flex';
|
| 403 |
+
const rp = document.getElementById('resPlaceholder');
|
| 404 |
+
if(rp) rp.style.display='none';
|
| 405 |
|
|
|
|
| 406 |
const pw = document.getElementById('progWrap'); pw.style.display='block';
|
| 407 |
const pb = document.getElementById('progBar');
|
| 408 |
let prog=0, pInterval = setInterval(()=>{ prog=Math.min(prog+Math.random()*8,88); pb.style.width=prog+'%'; },300);
|
|
|
|
| 409 |
const msgs=['Loading AI model…','Detecting edges…','Separating subject…','Refining mask…','Compositing result…'];
|
| 410 |
let mi=0, mInterval = setInterval(()=>{ const el=document.getElementById('procSub'); if(el) el.textContent=msgs[mi++%msgs.length]; },1500);
|
| 411 |
|
|
|
|
| 431 |
|
| 432 |
resultBlob = blob;
|
| 433 |
resultUrl = URL.createObjectURL(blob);
|
|
|
|
| 434 |
const elapsed = resp.headers.get('X-Processing-Time') || (Date.now()-t0);
|
| 435 |
const w = resp.headers.get('X-Image-Width')||'—';
|
| 436 |
const h = resp.headers.get('X-Image-Height')||'—';
|
|
|
|
| 446 |
document.getElementById('sDims').textContent=w+'×'+h;
|
| 447 |
document.getElementById('sTime').textContent=elapsed;
|
| 448 |
document.getElementById('sModel').textContent=selModel;
|
|
|
|
| 449 |
document.getElementById('resMeta').textContent=w+'×'+h+' '+document.getElementById('outFmt').value.toUpperCase();
|
|
|
|
| 450 |
document.getElementById('dlBtn').disabled=false;
|
| 451 |
document.getElementById('copyBtn').disabled=false;
|
| 452 |
addHistory(resultUrl, origFile.name);
|
| 453 |
if(curView==='compare') buildCompare();
|
| 454 |
+
toast('✓','Background removed!','success');
|
| 455 |
} catch(e){
|
| 456 |
clearInterval(pInterval); clearInterval(mInterval);
|
| 457 |
pw.style.display='none';
|
|
|
|
| 465 |
document.getElementById('procTxt').textContent='Remove Background';
|
| 466 |
}
|
| 467 |
|
|
|
|
| 468 |
function setModel(m,el){
|
| 469 |
selModel=m;
|
| 470 |
document.querySelectorAll('.model-btn').forEach(b=>b.classList.remove('active'));
|
|
|
|
| 477 |
document.getElementById('hexIn').value = c==='transparent'?'':c;
|
| 478 |
if(c!=='transparent'&&c.startsWith('#')) document.getElementById('cpicker').value=c;
|
| 479 |
}
|
| 480 |
+
function onHex(v){ if(/^#[0-9a-fA-F]{6}$/.test(v)) selSwatch(v,null); }
|
|
|
|
|
|
|
| 481 |
function toggleShadowOpts(on){ document.getElementById('shadowOpts').style.display=on?'block':'none'; }
|
| 482 |
|
|
|
|
| 483 |
function dlResult(){
|
| 484 |
if(!resultBlob) return;
|
| 485 |
const fmt = document.getElementById('outFmt').value;
|
| 486 |
const a = document.createElement('a');
|
| 487 |
+
a.href = resultUrl; a.download = 'cutout.'+fmt; a.click();
|
|
|
|
|
|
|
| 488 |
toast('⬇','Downloaded!','success');
|
| 489 |
}
|
| 490 |
|
|
|
|
| 493 |
try {
|
| 494 |
const png = resultBlob.type==='image/png' ? resultBlob : await convertToPng(resultBlob);
|
| 495 |
await navigator.clipboard.write([new ClipboardItem({'image/png':png})]);
|
| 496 |
+
toast('📋','Copied!','success');
|
| 497 |
} catch(e){ toast('⚠','Copy failed: '+e.message,'error'); }
|
| 498 |
}
|
|
|
|
| 499 |
async function convertToPng(blob){
|
| 500 |
const url=URL.createObjectURL(blob);
|
| 501 |
+
const img=await new Promise((res,rej)=>{ const i=new Image(); i.onload=()=>res(i); i.onerror=rej; i.src=url; });
|
| 502 |
const c=document.createElement('canvas'); c.width=img.width; c.height=img.height;
|
| 503 |
c.getContext('2d').drawImage(img,0,0);
|
| 504 |
return new Promise(res=>c.toBlob(res,'image/png'));
|
| 505 |
}
|
|
|
|
| 506 |
|
|
|
|
| 507 |
function sv(v){
|
| 508 |
curView=v;
|
| 509 |
['edit','compare'].forEach(x=>{ document.getElementById('v-'+x).style.display=x===v?'block':'none'; document.getElementById('t-'+x).classList.toggle('active',x===v); });
|
|
|
|
| 514 |
const c=document.getElementById('cmpContainer');
|
| 515 |
if(!resultUrl||!origDataUrl){ c.innerHTML='<div class="ph"><div class="ph-icon">⚡</div>Process an image first</div>'; return; }
|
| 516 |
c.innerHTML=`<div class="cmp-wrap" id="cw" style="max-width:640px;width:100%"><img src="${origDataUrl}" id="cb"/><div class="cmp-after" id="ca" style="clip-path:inset(0 50% 0 0)"><img src="${resultUrl}"/></div><div class="cmp-div" id="cd" style="left:50%"><div class="cmp-handle">↔</div></div><div class="cmp-labels"><span class="clabel">Original</span><span class="clabel">Result</span></div></div>`;
|
| 517 |
+
const wrap=document.getElementById('cw'),after=document.getElementById('ca'),div=document.getElementById('cd');
|
| 518 |
let drag=false;
|
| 519 |
wrap.addEventListener('mousedown',()=>drag=true);
|
| 520 |
document.addEventListener('mouseup',()=>drag=false);
|
| 521 |
+
document.addEventListener('mousemove',e=>{ if(!drag)return; const r=wrap.getBoundingClientRect(); const p=Math.max(0,Math.min(100,((e.clientX-r.left)/r.width)*100)); div.style.left=p+'%'; after.style.clipPath=`inset(0 ${100-p}% 0 0)`; });
|
| 522 |
+
wrap.addEventListener('touchmove',e=>{ const r=wrap.getBoundingClientRect(); const p=Math.max(0,Math.min(100,((e.touches[0].clientX-r.left)/r.width)*100)); div.style.left=p+'%'; after.style.clipPath=`inset(0 ${100-p}% 0 0)`; },{passive:true});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
}
|
| 524 |
|
|
|
|
| 525 |
function addHistory(url,name){
|
| 526 |
history.unshift({url,name});
|
| 527 |
if(history.length>16) history.pop();
|
|
|
|
| 539 |
toast('↩','Restored from history','success');
|
| 540 |
}
|
| 541 |
|
|
|
|
| 542 |
function toast(icon,msg,type=''){
|
| 543 |
const t=document.getElementById('toast');
|
| 544 |
document.getElementById('tIcon').textContent=icon;
|
|
|
|
| 547 |
t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),3500);
|
| 548 |
}
|
| 549 |
|
|
|
|
| 550 |
testServer();
|
| 551 |
</script>
|
| 552 |
</body>
|