by Audrey M. Roy Greenfeld | Tue, Jan 21, 2025
Exploring how to make basic SVG animations work with FastHTML.
from fastcore.meta import delegates from fastcore.utils import snake2camel from fasthtml.common import * from fasthtml.svg import *
The FastHTML SVG API Docs introduce you to fasthtml.svg
with this nice circle specified as an SVG string:
svg = '<svg width="50" height="50"><circle cx="20" cy="20" r="15" fill="red"></circle></svg>' show(NotStr(svg))
Often you'll just want to paste these strings into your FastHTML apps, and that's fine. However, when you want to construct SVG elements programmatically via Python, you can!
def demo(el, h=50, w=50): return show(Svg(h=h,w=w)(el))
demo(Rect(30, 30, fill='blue', rx=8, ry=8))
svg = """<svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg"> <rect width="10" height="10"> <animate attributeName="rx" values="0;5;0" dur="10s" repeatCount="indefinite" /> </rect> </svg> """
show(NotStr(svg))
Why is that rectangle so big here? Let's try
svg2 = """<svg width="100" height="100"> <rect width="10" height="10"> <animate attributeName="rx" values="0;5;0" dur="10s" repeatCount="indefinite" /> </rect> </svg> """ show(NotStr(svg2))
Currently Rect()
doesn't accept an animate
child. Seeing if I can make that work:
def Animate(attributeName, values, dur, repeatCount): return Safe(f"<animate {attributeName=} {values=} {dur=} {repeatCount=} />")
Animate(attributeName="rx", values="0;5;0", dur="10s", repeatCount="indefinite")
@delegates(ft_svg) def AnimatedRect(animate, width, height, x=0, y=0, fill=None, stroke=None, stroke_width=None, rx=None, ry=None, **kwargs): "An animated standard SVG `rect` element" return ft_svg('rect', animate, width=width, height=height, x=x, y=y, fill=fill, stroke=stroke, stroke_width=stroke_width, rx=rx, ry=ry, **kwargs)
show(Svg(AnimatedRect( Animate(attributeName="rx", values="0;5;0", dur="10s", repeatCount="indefinite"), width=10, height=10), h=10, w=10))
show(Svg(AnimatedRect( Animate(attributeName="rx", values="0;50;0", dur="10s", repeatCount="indefinite"), width=100, height=100), h=100, w=100))
demo(AnimatedRect( Animate(attributeName="rx", values="0;50;0", dur="1s", repeatCount="indefinite"), width=100, height=100), h=100, w=100)
svg4 = """<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"> <!-- Gradient definitions --> <defs> <linearGradient id="gradient1" x1="0%" y1="0%" x2="100%" y2="100%"> <stop offset="0%" stop-color="#60a5fa"> <animate attributeName="stop-color" values="#60a5fa;#8b5cf6;#ec4899;#60a5fa" dur="8s" repeatCount="indefinite" /> </stop> <stop offset="100%" stop-color="#8b5cf6"> <animate attributeName="stop-color" values="#8b5cf6;#ec4899;#60a5fa;#8b5cf6" dur="8s" repeatCount="indefinite" /> </stop> </linearGradient> </defs> <!-- Background star burst --> <g> <circle cx="50" cy="50" r="45" fill="url(#gradient1)"> <animate attributeName="opacity" values="0.3;0.5;0.3" dur="3s" repeatCount="indefinite" /> </circle> </g> <!-- Spinning triangles --> <g transform="translate(50 50)"> <path d="M0,-30 L26,15 L-26,15 Z" fill="#fcd34d" opacity="0.8"> <animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="8s" repeatCount="indefinite" /> <animate attributeName="d" values="M0,-30 L26,15 L-26,15 Z;M0,-20 L35,25 L-35,25 Z;M0,-30 L26,15 L-26,15 Z" dur="4s" repeatCount="indefinite" /> </path> </g> <!-- Bouncing squares --> <rect x="40" y="40" width="20" height="20" fill="#34d399" opacity="0.8"> <animate attributeName="x" values="40;30;50;40" dur="3s" repeatCount="indefinite" /> <animate attributeName="y" values="40;50;30;40" dur="3s" repeatCount="indefinite" /> <animate attributeName="rx" values="0;10;0" dur="3s" repeatCount="indefinite" /> </rect> <!-- Orbiting particles --> <g> <circle cx="50" cy="20" r="4" fill="#f472b6"> <animateMotion path="M 0,0 a 30,30 0 1,1 0,0" dur="3s" repeatCount="indefinite" /> <animate attributeName="r" values="4;6;4" dur="1.5s" repeatCount="indefinite" /> </circle> <circle cx="80" cy="50" r="4" fill="#60a5fa"> <animateMotion path="M 0,0 a 30,30 0 1,0 0,0" dur="4s" repeatCount="indefinite" /> <animate attributeName="r" values="4;6;4" dur="2s" repeatCount="indefinite" /> </circle> </g> <!-- Dancing dots --> <g> <circle cx="50" cy="50" r="3" fill="#fcd34d"> <animateMotion path="M 0,0 q 15,15 0,30 q -15,15 0,0" dur="2.5s" repeatCount="indefinite" /> </circle> <circle cx="50" cy="50" r="3" fill="#f472b6"> <animateMotion path="M 0,0 q -15,-15 -30,0 q -15,15 0,0" dur="2.5s" repeatCount="indefinite" /> </circle> </g> <!-- Pulsing rings --> <circle cx="50" cy="50" r="20" fill="none" stroke="#93c5fd" stroke-width="1"> <animate attributeName="r" values="20;30;20" dur="4s" repeatCount="indefinite" /> <animate attributeName="stroke-opacity" values="1;0;1" dur="4s" repeatCount="indefinite" /> </circle> <circle cx="50" cy="50" r="25" fill="none" stroke="#93c5fd" stroke-width="1"> <animate attributeName="r" values="25;35;25" dur="4s" repeatCount="indefinite" /> <animate attributeName="stroke-opacity" values="0;1;0" dur="4s" repeatCount="indefinite" /> </circle> </svg>""" show(NotStr(svg4))
I kind of like this one for representing "a sound is currently playing" for experimental audio apps.