File size: 3,096 Bytes
ffe3e3c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
"""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 ""