A blast from the past: in early 2014, we enabled Generational Garbage Collection (GGC) for Firefox desktop. But this blog post is not about GGC. It is about my victory dance when we finally pushed to button to let it go live on desktop firefox. Please click on that link; it is, after all, what this blog post is about.

Old-timers will recognize the old TBPL interface, since replaced with TreeHerder. I grabbed a snapshot of the page after I pushed GGC live (yes, green pushes really used to be that green!), then hacked it up a bit to implement the letter fly-in. And that fly-in is what I’d like to talk about now.

At the time I wrote it, I barely knew Javascript, and I knew even less CSS. But that was a long time ago.

Today… er, well, today I don’t know any more of either of those than I did back then.

Which is ok, since my whole goal here is to ask: what is the right way to implement that page? And specifically, would it be possible to do without any JS? Or perhaps minimal JS, just some glue between CSS animations or something?

To be specific: I am very particular about the animation I want. After the letters are done flying in, I want them to cycle through in the way shown. For example, they should be rotating around in the “O”. In general, they’re just repeatedly walking a path that is possibly discontinuous (as with any letter other than “O”). We’ll call this the marquee pattern.

Then when flying in, I want them to go to their appropriate positions within the marquee pattern. I don’t want them to fly to a starting position and only start moving in the marquee pattern once they get there. Oh noes, no no no. That would introduce a visible discontinuity. Plus which, the letters that started out close to their final position would move very slowly at first, then jerk into faster motion when the marquee began. We couldn’t have that now, could we?

I knew about CSS animations at the time I wrote this. But I couldn’t (and still can’t) see how to make use of them, at least without doing something crazy like redefining the animation every frame from JS. And in that case, why use CSS at all?

CSS can animate a smooth path between fixed points. So if I relaxed the constraint above (about the fly-in blending smoothly into the marquee pattern), I could pretty easily set up an animation to fly to the final position, then switch to a marquee animation. But how can you get the full effect? I speculated about some trick involving animating invisible elements with one animation, then having another animation to fly-in from each element’s original location to the corresponding invisible element’s marquee location, but I don’t know if that’s even possible.

You can look at the source code of the page. It’s a complete mess, combining as it does a cut and paste of the TBPL interface, plus all of jquery crammed in, and then finally my hacky code at the end of the file. Note that this was not easy code for me to write, and I did it when I got the idea at around 10pm during a JS work week, and it took me until about 4am. So don’t expect much in the way of comments or sanity or whatnot. In fact, the only comment that isn’t commenting out code appears to be the text “????!”.

The green letters have the CSS class “success”. There are lots of hardcoded coordinates, generated by hand-drawing the text in the Gimp and manually writing down the relevant coordinates. But my code doesn’t matter; the point is that there’s some messy Javascript called every frame to calculate the desired position of every one of those letters. Surely there’s a better way? (I should note that the animation is way smoother in today’s Nightly than it was back then. Progress!)

Anyway, the exact question is: how would someone implement this effect if they actually knew what they were doing?

I’ll take my answer off the air. In the comments, to be precise. Thank you in advance.

5 Responses to “Animation Done Wrong (aka Fix My Code, Please!)”

  1. Markus Stange Says:

    It’s not pretty, but it’s an idea: http://codepen.io/mstange/pen/vLGWVG

    I tried to separate the animation into an inner animation and an outer “envelope” animation: The envelope animation controls the position of the animation origin and the envelope scale, and the inner animation is what happens inside the moved/scaled envelope.
    Animating an offset independently from the rest of the animation is easy: You add a wrapper element and animate the position of that wrapper. Whatever happens inside the wrapper is relative to the wrapper.
    Adding a scale is much harder. I exploited the font-size property and the em unit for that purpose: The wrapper animates its font-size from 0 to 10px, and the inner animation then uses em units for expressing distance from the overall animation center.
    Unfortunately, this meant that I needed to add another wrapper so that I could unset the font-size of the actual letter particle.

  2. Stuart Says:

    I don’t know what I’m doing either, but my wild guess: how about ever-so-slightly relaxing the constraints on discontinuity and creating keyframes with the calculated positions for, say, each second or so? The acceleration and deceleration wouldn’t be perfectly smooth but it could avoid being totally jumpy that way.

    I suspect that doing it that way would still need JS to calculate the positions and construct the CSS animation (unless you hardcode knowledge of each letter’s starting position). But you could start with a pure-CSS version that includes the jumpy acceleration you dislike, with JS then switching in the corrected version, so that it still “works” but less smoothly in a pure CSS world.

  3. Stuart Says:

    After posting my prior comment I grokked some of the difficulties with the pure CSS approach, but I think I did figure out how to get *something* in pure CSS for if JS isn’t available to calculate smooth keyframes.

    Create two animations – one applies to span.flyin and one to span.marquee. Wrap each letter in X. Set the animation-delay on the marquee animation to the duration of the flyin animation. To avoid the letters all arriving at the start of the marquee at the exact same moment, the duration of the flyin (and delay of the marquee) should be staggered. This could be done with an ugly repetitive stylesheet

    .flyin.n1 { animation-duration: 3s }
    .flyin.n1 .marquee { animation-delay: 3s }
    .flyin.n2 { animation-duration: 3.1s; }
    .flyin.n2 .marquee { animation-delay: 3.1s }

    or you could apply data-num=”0″, “1”, “2”, … to BOTH spans and say
    .flyin { animation-duration: calc(3s + attr(data-num) * 0.1s); }
    .marquee { animation-delay: calc(3s + attr(data-num) * 0.1s); }
    but I’ve no idea how well that’s supported in browsers, and entering two data-num attributes in the html for each letter is only a little bit less annoying than entering two lines in the stylesheet for them!

  4. Stuart Says:

    You nerd-sniped me pretty bad with this one! I think I’ve got a theoretical pure CSS technique that gets 90% of the way to the result you want, but there seems to be one specific bit that’s impossible without JS.

    First, put a span around each letter that runs the marquee animation, with a delay adjusted to offset each letter to the right point on the path. The paths for the letter shape are expressed in percentages of the bounding box of the letter. The span is set to display:block, position:absolute.

    Wrapped that in another span with display:block, position:relative that’s initially styled to 0x0 pixels, so the marquee animation is having no effect because it’s animating percentages of zero. The letters still show up because overflow:visible is the default. This block gets an animation that kicks in once the flyin is already underway, scaling itself up to the right size for the big letter in a very smooth curve.

    The next container has position: absolute and gets the position part of the flyin animation, also using a smooth curve.

    To make sure that the whole thing still acts like a letter for purposes of initial layout, it needs another wrapper around it – this one inline-block and position relative again. Along with all the things previously mentioned, this contains a span with the same letter but styled to visibility:hidden so that the right amount of space for the letter is reserved in the main layout even after the real letter has flown away.

    (I thought it might be possible use ::first-line and ::first-letter so that fewer nested elements are needed, but it looks like the properties they support are too limited).

    The only part I can’t see how to do in CSS: the eventual position of the big letters needs to be in terms of position:fixed or absolute-relative-to-the-page-as-a-whole, but the elements to be moved need to start at coordinates relative to their actual position in the layout. CSS seems to have no way to animate smoothly between position:absolute/relative coordinates and position:fixed ones, and I can’t see any clever way to combine nested elements in different coordinate systems to achieve that part of the effect.

  5. Steve Fink Says:

    Wow, this is great! Ask and ye shall receive! (Or the world rewards laziness, or something.)

    @mstange: using em units for scaling is brilliant. And your demo page is buttery smooth on my Nightly (well, I did see one or two jumps while playing with it, but that seems about as good as you can get for now.)

    @Stuart: sorry about the nerd-sniping! Well, ok, not that sorry. Fine, not sorry at all. I can pretend to be sorry if you’d like.

    The JS restriction wasn’t so much about supporting non-JS browsers, though come to think of it that’s a great motivation. But for me, it was more about the smoothness of the animation. I assume that the more things that happen in CSS, the less prone it should be to jerks and stalls. Which means that I’m ok with a bunch of heavy JS setup code, as long as the final result doesn’t require JS to run for each frame. But as you said, there are other good reasons to want to avoid JS.

    I’ve read through your comments a couple of times. I’m still digesting. (I wasn’t kidding about not knowing much CSS.)

    Thanks!

Leave a Reply