Skip to content

Animations - #127

Draft
georgefst wants to merge 4 commits into
diagrams:masterfrom
georgefst:animations
Draft

Animations#127
georgefst wants to merge 4 commits into
diagrams:masterfrom
georgefst:animations

Conversation

@georgefst

@georgefst georgefst commented Oct 17, 2025

Copy link
Copy Markdown
Contributor

This is obviously a work-in-progress, but I wanted to get feedback as early as possible, in particular on whether something like this is likely to be accepted upstream.

This uses the animateTransform element to animate arbitrary subdiagrams. It's part of the standard, though unfortunately not yet widely supported by non-web SVG viewers (I've been using Gnome's Epiphany web browser for testing, as it's the only tool I've found which shows animations and auto-reloads on changes to a local file). We should also support animateMotion, potentially taking a Diagrams Path, as this allows for translations along a smooth curve. There's also animate, but that seems too ad-hoc to be useful in the context of Diagrams.

I've linked to MDN there, but actually much of this API is poorly-documented everywhere but the official spec. In particular, I've seen no good explanation elsewhere of the values attribute, which we use here as it's necessary for any animations which go through multiple states, rather than just smoothly from A to B. We should check this spec to make sure we cover everything important in our API surface.

@byorgey I know you've hinted at developing a completely new approach to animations in Diagrams for a long time. I'd love to know more about what sort of API you're aiming for, as I've been unable to find any details. I have always assumed it's still fundamentally similar to the general-but-inefficient "lists of diagrams" stuff that one sees in e.g. diagrams-rasterific for GIF generation. But perhaps you had an abstraction in mind which would support native animations in the SVG backend?

Regardless, this SVG-specific approach has been highly useful, especially with Reanimate seemingly no longer maintained, and it's allowed us to avoid needing #126.

CC @patrickaldis @cgibbard

Comment thread src/Diagrams/Backend/SVG.hs Outdated
let (transOriginAttr, transElement) = case getAttr sty of
Nothing -> ([], mempty)
Just (TransformAnimationAttribute animations t) ->
( pure . makeAttribute "transform-origin" . (\(P (V2 x y)) -> showNum x <> " " <> showNum y) $ transform t 1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks very slightly off for my working example (by about V2 -3.3 3.3 for small circles, tending to 0 for larger ones). Not sure why. I guess I should come up with a simpler example anyway, to post in this thread as motivation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I fixed this by changing 1 to 0, which makes sense as we should only care about translations to the origin.

, flip foldMap animations $ \(TransformAnimation dur rep animation) -> animateTransform_ $
[ AttributeName_ <<- "transform"
, Additive_ <<- "sum"
, Dur_ <<- T.show dur <> "s"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, T.show is causing CI to fail for all GHCs before 9.12. Guess I'll have to do this the old-fashioned way.

gDefs `mappend` g_ (R.renderStyles idFill idLine sty) clippedSvg
gDefs `mappend` g_ (transOriginAttr <> R.renderStyles idFill idLine sty) (transElement <> clippedSvg)
where
showNum = T.pack . showFixed @E3 True . realToFrac

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone with three decimal places based on looking at existing numbers in the SVG output. Presumably there's some code for those which I should be reusing here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember!

TransformAnimationType
data TransformAnimationType
= ScaleAnimation [V2 Double]
| TranslateAnimation [V2 Double]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use stronger types here, possibly records, better names, add support for rotations etc...

But actually, perhaps we should just generalise and take actual transformations, i.e. a [T2 Double]? Can all transformations be decomposed in to a combination of scaling, translating and rotating and maybe skewing? As these are the operations that <animateTransform> accepts.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, all affine transformations can be decomposed into a combination of translation + rotation + scaling + shear (and shear can itself actually be decomposed into rotation + scaling), though there is not necessarily a unique decomposition. For example, see https://research.cs.wisc.edu/graphics/Courses/838-s2002/Papers/polar-decomp.pdf .

I don't understand this code well enough yet to have an opinion on whether generalizing to take actual transformations would be a good idea.

type instance N TransformAnimationAttribute = Double
instance Semigroup TransformAnimationAttribute where
TransformAnimationAttribute xs tx <> TransformAnimationAttribute ys ty =
TransformAnimationAttribute (xs <> ys) (tx <> ty)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this is correct... But maybe if we moved to taking actual transformations as mentioned above, this would sort of solve itself.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why wouldn't this be correct? The mapping from [TransformAnimation] to Transformation V2 Double should be a monoid homomorphism, so this should work just fine (unless the order is wrong? I don't understand it well enough yet to say whether it should be tx <> ty or ty <> tx, but I do know that it bears careful thinking about).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why wouldn't this be correct?

Simply because I haven't thought about it deeply, or heavily tested it yet. I just wrote the obvious thing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, OK, given #127 (comment) , then I don't think this is correct. Transformations do not commute in general, and simply composing the two different transformations separately does not seem to respect or consider the order in which they should be applied. Perhaps some kind of semidirect product is needed, but I am not sure at the moment.

Co-authored-by: Patrick Aldis <patrickaldis@users.noreply.github.com>
TransformAnimationAttribute (xs <> ys) (tx <> ty)
instance Transformable TransformAnimationAttribute where
transform t (TransformAnimationAttribute a t0) =
TransformAnimationAttribute a (t <> t0)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, on the other hand, does not seem right at all. Applying a transformation to the stored Transform V2 Double but doing nothing to the a breaks the invariant that the stored Transform corresponds to the list of TransformAnimations, and after that point all bets are off.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait a minute, maybe that is not the invariant after all...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait a minute, maybe that is not the invariant after all...

Yes, that's not the intention. See #127 (comment).


data TransformAnimationAttribute = TransformAnimationAttribute
[TransformAnimation]
(Transformation V2 Double)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do the fields of the TransformAnimationAttribute data type represent?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first field is the transformations we want to apply, TransformAnimation corresponding to an <animateTransform>.

The second field keeps track of the transformations which have been applied to the animated element. You can see that we use it in attributedRender for setting the transform origin and for rescaling translations. There might be a more direct way to get hold of this? I'm not really that familiar with Diagrams' internals.

@byorgey

byorgey commented Oct 25, 2025

Copy link
Copy Markdown
Member

I know you've hinted at developing a completely new approach to animations in Diagrams for a long time. I'd love to know more about what sort of API you're aiming for, as I've been unable to find any details.

@georgefst it's not secret but you have to know where to look: the master branch of the active library, https://github.com/diagrams/active/ , which is a complete rewrite compared to the currently released version of the active package. Getting active cleaned up and actually released, in conjunction with the necessary changes to diagrams-lib, has been on my list for an extremely long time now (5+ years), it's just never risen quite above other priorities...

The basic semantics of Active values are functions from time to diagrams (or whatever), suitable for sampling to generate a list of frames. However, the Active data type is actually represented via a deep embedding, with the explicit goal of making possible things like directly generating SVG animations. It's been a long time since I've thought about this, but perhaps we could set up a video chat sometime to talk about the possibilities.

@georgefst

Copy link
Copy Markdown
Contributor Author

Getting active cleaned up and actually released, in conjunction with the necessary changes to diagrams-lib, has been on my list for an extremely long time now (5+ years), it's just never risen quite above other priorities...

I know that feeling!

It's been a long time since I've thought about this, but perhaps we could set up a video chat sometime to talk about the possibilities.

I would love to get in to it, but I don't know if I'll have the time. It's unlikely I can carve it out during work hours, since this already does what we need for now for prototyping, and we'll probably move to a different approach once we need interactivity.

I know it's a long way off, but are you likely to be at Zurihac again in June? I'd hoped to say hi this year after your talk, but I was unfortunately stuck in my hotel for most of the weekend due to illness.

@georgefst

Copy link
Copy Markdown
Contributor Author

we'll probably move to a different approach once we need interactivity

Unless Active were flexible enough that an appropriate diagrams-reflex instance could enable this...

@byorgey

byorgey commented Oct 30, 2025

Copy link
Copy Markdown
Member

I would love to get in to it, but I don't know if I'll have the time.

Fair enough!

I know it's a long way off, but are you likely to be at Zurihac again in June?

It's unlikely, unless someone wants to pay for me to come (last year ZuriHac paid for my travel since I was an invited speaker). Being at a small institution I don't have much funding for international travel.

Unless Active were flexible enough that an appropriate diagrams-reflex instance could enable this...

It very well could be! That is, I certainly aspire/intend for it to be. But I don't know how much work would still be required to get everything to work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants