manim / backend /renderer.py
Bandrik0
tweak(draw): fill off + auto-stroke + lagged Create
f2a8d14
import os
import tempfile
import subprocess
from pathlib import Path
from fastapi import UploadFile
_QUALITY = {"l": "l", "m": "m", "h": "h", "u": "u"}
def _save_upload(tmpdir: Path, file: UploadFile) -> Path:
ext = Path(file.filename or "file").suffix.lower()
out = tmpdir / f"input{ext}"
with out.open("wb") as f:
f.write(file.file.read())
return out
def _gen_scene_py(path: Path, anim: str, duration: float, bg_color: str) -> Path:
is_svg = path.suffix.lower() == ".svg"
safe_path = repr(str(path))
if anim == "draw" and not is_svg:
raise ValueError("Draw modu yalnızca SVG kabul eder.")
head = f"""from manim import *
class LogoScene(Scene):
def construct(self):
self.camera.background_color = "{bg_color}"
"""
if anim == "draw":
# Fill'leri kapat + stroke yoksa hafif bir stroke ata + parçalı çiz
body = f"""
m = SVGMobject({safe_path}, should_center=True).set_height(5).move_to(ORIGIN)
# family_members_with_points: gerçekten çizilebilir alt-objeler
for sm in m.family_members_with_points():
# fill kapalı başlasın ki Create sırasında sadece çizgi görünsün
sm.set_fill(opacity=0)
# stroke yoksa otomatik ver (birçok SVG'de yalnızca fill var)
if sm.get_stroke_width() == 0:
sm.set_stroke(color=WHITE, width=3, opacity=1)
# parçalı çizim (birbirinin üstüne binecek şekilde)
self.play(
LaggedStartMap(Create, m.family_members_with_points(), lag_ratio=0.04, rate_func=linear),
run_time={float(duration)}
)
# çizim bittikten sonra dolguları geri getir
self.play(m.animate.set_fill(opacity=1), run_time=0.6)
self.wait(0.2)
"""
else:
loader = (
f'SVGMobject({safe_path}, should_center=True).set_height(5)'
if is_svg else
f'ImageMobject({safe_path}).scale(0.9)'
)
body_start = f"""m = {loader}
m.move_to(ORIGIN)
"""
if anim == "fade":
body_anim = f"""
self.play(FadeIn(m), run_time=max(0.3, {float(duration)}*0.5))
self.wait(0.1)
self.play(FadeOut(m), run_time=max(0.2, {float(duration)}*0.3))
"""
elif anim == "spin":
body_anim = f"""
self.play(FadeIn(m, scale=0.8), run_time=max(0.2, {float(duration)}*0.2))
self.play(Rotate(m, angle=2*PI), rate_func=linear, run_time=max(0.4, {float(duration)}*0.6))
self.play(FadeOut(m), run_time=max(0.1, {float(duration)}*0.2))
"""
elif anim == "bounce":
body_anim = f"""
from manim import UP, DOWN, there_and_back
self.play(FadeIn(m, shift=UP*3), run_time=max(0.2, {float(duration)}*0.25))
self.play(m.animate.shift(DOWN*3).scale(1.02), rate_func=there_and_back, run_time=max(0.4, {float(duration)}*0.55))
self.play(FadeOut(m), run_time=max(0.15, {float(duration)}*0.2))
"""
else:
raise ValueError("Geçersiz animasyon tipi.")
body = body_start + body_anim
code = head + body + "\n"
scene_py = path.parent / "logo_scene.py"
scene_py.write_text(code, encoding="utf-8")
return scene_py
def _run_manim(scene_py: Path, quality: str) -> Path:
q = _QUALITY.get(quality, "m")
tmpdir = scene_py.parent
out_name = "out.mp4"
cmd = [
"manim", "-q", q,
str(scene_py), "LogoScene",
"-o", out_name,
"--format=mp4",
"--media_dir", str(tmpdir),
]
proc = subprocess.run(cmd, cwd=tmpdir, capture_output=True, text=True)
if proc.returncode != 0:
raise RuntimeError(f"Manim failed: {proc.stderr or proc.stdout}")
for root, _, files in os.walk(tmpdir):
if out_name in files:
return Path(root) / out_name
raise RuntimeError("Çıktı mp4 bulunamadı.")
async def render_logo(file: UploadFile, animation: str, duration: float, bg_color: str, quality: str) -> Path:
with tempfile.TemporaryDirectory() as tdir:
tdir = Path(tdir)
input_path = _save_upload(tdir, file)
scene_py = _gen_scene_py(input_path, animation, duration, bg_color)
mp4 = _run_manim(scene_py, quality)
final_dir = Path("/tmp/manim_out")
final_dir.mkdir(parents=True, exist_ok=True)
final_path = final_dir / mp4.name
final_path.write_bytes(mp4.read_bytes())
return final_path