manim / frontend /index.html
Bandrik0
root redirect + relative API URL for HF Space
d775c67
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Manim Logo Renderer</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body {
margin: 0;
font-family: ui-sans-serif, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
color: #e8eaed;
background:
radial-gradient(80rem 60rem at 10% -20%, #0f172a 10%, transparent 60%),
radial-gradient(60rem 50rem at 110% 10%, #1b1b2f 10%, transparent 60%),
linear-gradient(180deg, #0a0a0a, #0b0b0f 60%, #0a0a0a);
min-height: 100svh;
}
.wrap { max-width: 1100px; margin: 48px auto; padding: 0 20px; }
header { display:flex; align-items:center; justify-content:space-between; gap:16px; margin-bottom:18px; }
h1 { margin: 0; font-size: 28px; letter-spacing:.2px; }
.pill { font-size:13px; opacity:.8 }
.card {
border: 1px solid #23262f;
background: linear-gradient(180deg, rgba(255,255,255,.02), rgba(255,255,255,.01));
backdrop-filter: blur(6px);
border-radius: 18px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,.35);
}
.grid { display:grid; grid-template-columns: 1.05fr .95fr; gap:18px; }
@media (max-width: 900px){ .grid { grid-template-columns: 1fr; } }
.drop {
border: 2px dashed #3a3f55; border-radius: 14px; padding: 28px; text-align:center;
background: #0e1220; transition: all .25s ease; position: relative; overflow: hidden;
}
.drop::after { content:""; position:absolute; inset:-1px;
background: radial-gradient(60% 60% at 20% 0%, rgba(59,130,246,.15), transparent 60%); mix-blend-mode:screen; pointer-events:none; }
.drop.drag { border-color:#8ab4ff; background:#0d1228; }
input[type="file"] { display:none; }
.btn { background:#3b82f6; color:#fff; border:0; border-radius:12px; padding:12px 16px; font-weight:700; cursor:pointer; transition:transform .04s, opacity .25s; }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity:.6; cursor:not-allowed; }
.row { display:flex; flex-direction:column; gap:10px; }
.opts { display:grid; gap:8px; grid-template-columns: repeat(4, minmax(0,1fr)); }
label.sel { background:#0f1426; padding:12px; border-radius:12px; border:1px solid #262b40; display:flex; gap:8px; align-items:center; justify-content:center; cursor:pointer; }
label.sel:hover { border-color:#3b82f6; background:#0f1733; }
input[type="number"], select, input[type="text"] { border-radius:12px; border:1px solid #2a3150; background:#0b1022; color:#eaeaea; padding:10px 12px; outline:none; }
.status { margin-top:14px; padding:14px; border-radius:14px; background:#0d1224; border:1px solid #22273a; }
.stages { display:flex; gap:10px; align-items:center; justify-content:space-between; }
.stage { flex:1; text-align:center; font-size:12px; opacity:.65; }
.stage .dot { width:10px; height:10px; border-radius:50%; display:inline-block; margin-bottom:6px; background:#3a415c; box-shadow:0 0 0 2px #1a1f33 inset; }
.stage.active .dot { background:#60a5fa; box-shadow:0 0 10px rgba(96,165,250,.5); }
.stage.done .dot { background:#22c55e; box-shadow:0 0 10px rgba(34,197,94,.45); }
.progress { margin-top:10px; height:10px; border-radius:999px; background:#1b233a; position:relative; overflow:hidden; border:1px solid #232a45; }
.bar { position:absolute; left:0; top:0; bottom:0; width:0%; background:linear-gradient(90deg,#2563eb,#60a5fa,#22d3ee); filter: drop-shadow(0 0 10px rgba(59,130,246,.5)); transition:width .25s ease; }
.video { margin-top:16px; }
.note { font-size:12px; opacity:.7; }
.toast { position:fixed; right:16px; bottom:16px; display:flex; gap:10px; align-items:center; background:#0f1426; border:1px solid #273050; padding:12px 14px; border-radius:12px; box-shadow:0 6px 20px rgba(0,0,0,.35); transform:translateY(20px); opacity:0; pointer-events:none; transition:all .25s; }
.toast.show { transform:translateY(0); opacity:1; pointer-events:auto; }
.toast .dot { width:10px; height:10px; border-radius:50%; background:#22c55e; box-shadow:0 0 12px rgba(34,197,94,.5); }
.toast.error .dot { background:#ef4444; box-shadow:0 0 12px rgba(239,68,68,.5); }
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Manim Logo Renderer</h1>
<div class="pill">SVG für "Draw" empfohlen • PNG/JPG: Fade/Spin/Bounce</div>
</header>
<div class="card">
<div class="grid">
<div class="row">
<div id="drop" class="drop">
<p style="margin-top:0"><strong>Datei hier ablegen</strong> oder</p>
<label class="btn"><input id="file" type="file" accept=".svg,.png,.jpg,.jpeg,.webp" />Datei wählen</label>
<div id="name" style="opacity:.8;margin-top:10px;"></div>
<div class="note" style="margin-top:8px;">Für Outline‑Draw bitte SVG laden.</div>
</div>
<div class="status" id="statusBox" style="display:none;">
<div class="stages">
<div class="stage" data-stage="upload"><span class="dot"></span><span>Uploading</span></div>
<div class="stage" data-stage="render"><span class="dot"></span><span>Rendering</span></div>
<div class="stage" data-stage="polish"><span class="dot"></span><span>Polishing</span></div>
<div class="stage" data-stage="done"><span class="dot"></span><span>Done</span></div>
</div>
<div class="progress"><div class="bar" id="bar"></div></div>
</div>
<div class="video" id="videoWrap" style="display:none;">
<video id="vid" controls playsinline style="width:100%; border-radius:12px; border:1px solid #2a2a2a;"></video>
<div style="display:flex; gap:10px; flex-wrap:wrap; margin-top:10px;">
<a id="dl" download="logo_animation.mp4" class="btn">Download</a>
<button id="again" class="btn" style="background:#10b981;">Render another</button>
</div>
</div>
</div>
<div class="row">
<label>Animation</label>
<div class="opts">
<label class="sel"><input type="radio" name="anim" value="draw" checked /> draw</label>
<label class="sel"><input type="radio" name="anim" value="fade" /> fade</label>
<label class="sel"><input type="radio" name="anim" value="spin" /> spin</label>
<label class="sel"><input type="radio" name="anim" value="bounce" /> bounce</label>
</div>
<label style="margin-top:12px;">Dauer (Sekunden)</label>
<input id="duration" type="number" min="1" step="0.5" value="4">
<label style="margin-top:12px;">Hintergrundfarbe</label>
<input id="bg" type="text" value="#111111" />
<label style="margin-top:12px;">Qualität</label>
<select id="quality">
<option value="l">Low</option>
<option value="m" selected>Medium</option>
<option value="h">High</option>
<option value="u">Ultra</option>
</select>
<button id="go" class="btn" style="margin-top:16px;">Rendern</button>
</div>
</div>
</div>
</div>
<div class="toast" id="toast"><span class="dot"></span><span id="toastMsg">Fertig!</span></div>
<script>
const API_URL = ""; // IMPORTANT: Hugging Face'de relative kalsın
const fileInput = document.getElementById('file');
const drop = document.getElementById('drop');
const nameEl = document.getElementById('name');
const go = document.getElementById('go');
const vid = document.getElementById('vid');
const videoWrap = document.getElementById('videoWrap');
const dl = document.getElementById('dl');
const again = document.getElementById('again');
const statusBox = document.getElementById('statusBox');
const bar = document.getElementById('bar');
const toast = document.getElementById('toast');
const toastMsg = document.getElementById('toastMsg');
let file;
function setFile(f) { file = f; nameEl.textContent = file ? file.name : ""; }
function setStage(stage) {
statusBox.style.display = 'block';
document.querySelectorAll('.stage').forEach(s => s.classList.remove('active','done'));
const stages = ['upload','render','polish','done'];
const idx = stages.indexOf(stage);
stages.forEach((st, i) => {
const el = document.querySelector(`.stage[data-stage="${st}"]`);
if (!el) return;
if (i < idx) el.classList.add('done');
if (i === idx) el.classList.add('active');
});
const widths = { upload:18, render:66, polish:88, done:100 };
bar.style.width = (widths[stage] || 0) + '%';
}
function showToast(msg, err=false) {
toastMsg.textContent = msg;
if (err) toast.classList.add('error'); else toast.classList.remove('error');
toast.classList.add('show'); setTimeout(()=>toast.classList.remove('show'), 2600);
}
drop.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('drag'); });
drop.addEventListener('dragleave', () => drop.classList.remove('drag'));
drop.addEventListener('drop', e => { e.preventDefault(); drop.classList.remove('drag'); if (e.dataTransfer.files[0]) setFile(e.dataTransfer.files[0]); });
fileInput.addEventListener('change', e => { if (e.target.files[0]) setFile(e.target.files[0]); });
again?.addEventListener('click', () => {
videoWrap.style.display = 'none'; statusBox.style.display = 'none'; bar.style.width = '0%';
vid.removeAttribute('src'); dl.removeAttribute('href');
});
go.addEventListener('click', async () => {
if (!file) { showToast("Bitte zuerst eine Datei wählen.", true); return; }
const anim = document.querySelector('input[name="anim"]:checked').value;
const duration = document.getElementById('duration').value;
const bg = document.getElementById('bg').value || "#111111";
const quality = document.getElementById('quality').value;
go.disabled = true; go.textContent = "Uploading…"; setStage('upload');
const fd = new FormData();
fd.append('file', file);
fd.append('animation', anim);
fd.append('duration', duration);
fd.append('bg_color', bg);
fd.append('quality', quality);
await new Promise(r => setTimeout(r, 350));
try {
setStage('render'); go.textContent = "Rendering…";
const res = await fetch(API_URL + "/render", { method: "POST", body: fd });
if (!res.ok) { const t = await res.text(); throw new Error(t || "Fehler beim Rendern"); }
const blob = await res.blob();
setStage('polish'); go.textContent = "Polishing…"; await new Promise(r => setTimeout(r, 500));
const url = URL.createObjectURL(blob);
vid.src = url; videoWrap.style.display = "block"; dl.href = url;
setStage('done'); go.textContent = "Rendern"; showToast("Fertig! MP4 bereit.");
} catch (err) {
console.error(err); showToast("Fehler: " + (err.message || err), true); go.textContent = "Rendern";
} finally { go.disabled = false; }
});
</script>
</body>
</html>