|
|
<!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 = ""; |
|
|
|
|
|
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> |