Home > Software engineering >  How do I get Elm animator to animate SVG elements using CSS animations?
How do I get Elm animator to animate SVG elements using CSS animations?

Time:01-16

I'm trying to do CSS animation in Elm, and I just can't get it to work!

Elm has several animation packages. The one I'm attempting to use is mdgriffith/elm-animator. Sadly, like many Elm packages, its documentation is sparse. It appears it provides two ways to render: one by running an Elm event loop to do a DOM update each frame, and one using CSS animation [which I didn't realise even existed]. I'm trying to do the latter.

I've tried to shrink the code down to a minimal example:

module Main exposing (main)

import Time
import Platform.Cmd
import Browser
import Html
import Html.Attributes
import Html.Events
import Color
import Svg
import Svg.Attributes
import Animator
import Animator.Css

main =
  Browser.document
  {
    init          = \        () -> ({fill = Animator.init <| Color.rgb 1 0 0}, Platform.Cmd.none),
    subscriptions = \     state -> Animator.toSubscription Tick state animator,
    view          = \     state -> {title = "Animation test", body = view state},
    update        = \ msg state -> (update msg state, Platform.Cmd.none)
  }

type alias State = { fill : Animator.Timeline Color.Color }

animator : Animator.Animator State
animator =
  Animator.animator
    |> Animator.Css.watching (\ state -> state.fill) (\ c state -> { state | fill = c })

type Msg = Tick Time.Posix | DoSomething

update : Msg -> State -> State
update msg state0 =
  case msg of
    Tick      t -> Animator.update t animator state0
    DoSomething -> { state0 | fill = Animator.go Animator.slowly (Color.rgb 1 1 0) state0.fill }

view : State -> List (Html.Html Msg)
view state0 =
  [
    Html.button [Html.Events.onClick DoSomething] [Html.text "Do something"],
    Svg.svg
      [
        Svg.Attributes.width  "100px",
        Svg.Attributes.height "100px"
      ]
      [
        Animator.Css.node "circle"
          state0.fill
          [
            Animator.Css.color
              "fill"
              (\ x -> x)
          ]
          [
            Svg.Attributes.cx "50",
            Svg.Attributes.cy "50",
            Svg.Attributes.r  "50"
          ]
          []
      ]
  ]

I appreciate that's still pretty big, but I don't see how to easily make it much shorter without obfuscating what it's doing.

When I run this, the SVG fails to render. When I click the button, nothing happens.

Here's the really weird part: If I open the Firefox Inspector window, I can see the SVG <circle> element. If I edit its various properties, nothing happens. However, if I right-click the <circle> and select "Edit as HTML...", then add a single space to the element text and click off it, suddenly the circle appears! Moreover, if I right-click the element again, now it shows as "Edit as SVG...", which is suspicious.

Even more fun: If I load the page, click the button, and then do the above trick, the colour animation runs!

Note that if I edit the HTML and change nothing, it doesn't work. I have to make a trivial change to the text (or else I guess the browser decides it doesn't need to reprocess?)

I was so convinced this was going to end up being a weird Firefox bug... but then I tried it in Chrome and Edge, and it does exactly the same thing!

Does anybody have to vaguest clue why this doesn't work? Have I done something wrong with the Elm code? (I'm really, really struggling to figure out how this library works; I'm basically just guessing how the types fit together. So maybe I've done something dumb.)

CodePudding user response:

This is due to a weird thing going on with namespaces. In an HTML document (i.e. on any webpage), the default namespace is the HTML namespace and if you want to render embedded documents in other formats, you need to ensure the nodes are created in the correct namespace.

Now when you write HTML as a string and the browser parses it from text, it will do this automatically for you:

<div> <!-- HTML Namespace -->
  <svg> 
    <circle /> <!-- SVG Namespace -->
  </svg>
  <math>
    <mrow></mrow> <!-- MathML namespace 
                  (not really relevant, just pointing out different NS) -->
  </math>
</div>

However, when you are constructing nodes dynamically (i.e. from JS or Elm), you need to explicitly ask for the namespace. That is in JS you would use Document.createElementNS() instead of just Document.createElement(). If you look at the source of the elm/svg package you will see that it does this:

node : String -> List (Attribute msg) -> List (Svg msg) -> Svg msg
node =
  VirtualDom.nodeNS "http://www.w3.org/2000/svg"

If you don't do this, the browser understands you want to create a new HTML element called circle, which the browser doesn't know about. If the browser doesn't know about an element, it just treats basically like a <div>.

Now if you look at the source of the elm-animator package, you'll notice that it asks for an Html.node, so no namespace!


As to how to make it work with elm-animator, I suspect you can't. But perhaps someone else can contribute some solution...


Finally, you might want to consider SMIL style animation, it tends to get the job done without needing any external package.

  • Related