ditherforge-api / svg_builder.py
wisamWR's picture
Initial: DitherForge API endpoints
ffe3e3c
Raw History Blame Contribute Delete
3.1 kB
"""SVG generation with optional CSS animations."""
from __future__ import annotations
from html import escape
def text_to_svg(
text: str,
font_size: int = 14,
color: str = "#00ff41",
bg_color: str = "#0d1117",
animation: str = "none",
duration: float = 2.0,
) -> str:
lines = text.split("\n")
cols = max((len(line) for line in lines), default=0)
char_w = font_size * 0.6
line_h = font_size * 1.2
svg_w = int(cols * char_w)
svg_h = int(len(lines) * line_h)
parts = [
f'<svg xmlns="http://www.w3.org/2000/svg" width="{svg_w}" height="{svg_h}" '
f'viewBox="0 0 {svg_w} {svg_h}">',
f'<rect width="100%" height="100%" fill="{bg_color}"/>',
]
css = _animation_css(animation, duration, len(lines))
if css:
parts.append(f"<style>{css}</style>")
use_row_class = animation in ("reveal", "matrix")
parts.append(
f'<text font-family="monospace" font-size="{font_size}" fill="{color}">'
)
for i, line in enumerate(lines):
y = (i + 1) * line_h - (line_h - font_size) / 2
cls = f' class="row row-{i}"' if use_row_class else ""
parts.append(f'<tspan x="0" y="{y:.1f}"{cls}>{escape(line)}</tspan>')
parts.append("</text>")
if animation == "scanline":
parts.append(
f'<rect class="scanline" x="0" y="0" width="100%" '
f'height="{line_h}" fill="{color}" opacity="0.15"/>'
)
parts.append("</svg>")
return "".join(parts)
def _animation_css(animation: str, duration: float, rows: int) -> str:
if animation == "reveal":
delays = "\n".join(
f".row-{i} {{ animation-delay: {i * duration / rows:.2f}s; }}"
for i in range(rows)
)
return (
".row { opacity: 0; animation: reveal 0.15s ease-out forwards; }\n"
"@keyframes reveal { from { opacity: 0; } to { opacity: 1; } }\n"
+ delays
)
if animation == "scanline":
return (
f"@keyframes scan {{ 0% {{ transform: translateY(-5%); }} "
f"100% {{ transform: translateY(105%); }} }}\n"
f".scanline {{ animation: scan {duration}s linear infinite; }}"
)
if animation == "flicker":
return (
"@keyframes flicker { 0%,100% { opacity:1; } "
"92% { opacity:1; } 93% { opacity:0.3; } "
"94% { opacity:1; } 96% { opacity:0.5; } 97% { opacity:1; } }\n"
f"text {{ animation: flicker {duration}s ease-in-out infinite; }}"
)
if animation == "matrix":
delays = "\n".join(
f".row-{i} {{ animation-delay: {i * duration / rows:.2f}s; }}"
for i in range(rows)
)
return (
f".row {{ opacity: 0; animation: matrix {duration * 0.3:.2f}s ease-out forwards; }}\n"
"@keyframes matrix { "
"0% { opacity:0; transform:translateY(-20px); } "
"60% { opacity:1; } "
"100% { opacity:1; transform:translateY(0); } }\n"
+ delays
)
return ""