JAKE ARCHIBALD: OK. Audio synchronization
time, 3, 2, 1. DAS SURMA: 3, 2, 1. JAKE ARCHIBALD: Brilliant. DAS SURMA: It's a little
bit like a Power Ranger. It's like, audio synchronization
time, [GRUNTING].. JAKE ARCHIBALD: The
worst Power Rangers ever. The power to synchronize audio. [MUSIC PLAYING] So I've been doing animation
on the web for probably about 20 years now. I used to write
a library for it, the BBC used back
when I worked there. I'd say more recently,
most of the animation work I do is like, when
I do slides for talks. It's one of the times I really
throw lots of weird animation effects and stuff to try
and keep people awake while I'm droning on
about something boring and standardsy. But I don't normally give my
credentials up front like that, but the reason I'm doing
that now is because I still get caught out by loads of
animation stuff on the web. I think it's really
hard, and I think that some of the tools
we have at our disposal are not so great and
could be a lot better. DAS SURMA: Yeah, this rings
very much true to home for me. True to home? Is that a thing? I think I just mis-Englished. Either way, but yeah, I remember
when we were writing procs that I had to write an animation
library for the Canvas space animations, and you were
like, I've done this before. Let me tell you of
all the struggles you're going to encounter. JAKE ARCHIBALD: Yes, exactly. So let's talk about two
relatively basic animations. Fading something in,
you just saw that there. And fading something
out, there you go. But even those have a ton
of complexity and gotchas. Yeah. I only feel like I'm starting to
come to some conclusions of how these basic animations should
be done in a way that's safe and avoids all of the gotchas. I would say way back when, I
would use jQuery or GreenSock or the library I wrote. I used GreenSock because I
used to be a Flash developer, I don't know if I've
mentioned that enough times, but that GreenSock was an
animation library for Flash that then moved to the web. So I used that for a bit. But then we got these
things, CSS transitions. So the older animation
libraries were just changing inline
styles over time. Either by using set timeout or,
later, request animation frame, which would be the
proper way of doing it. But with this you
got compositing, and the animation happening
off the main thread. So opacity and transforms, even
if your main thread is janky, these animations will
run nice and smooth. Pretty good for things
like hover effects, because no JavaScript needed. There you go. You can throw out your jQuery
and your GreenSock or whatever, and that's what I did. And I thought, I'm just
going to use these. This is it from now on. But I was also using
JavaScript for interactions, so I wanted to drive
transitions with JavaScript. So the user clicks
a button, and now I want the new thing to fade in. So I'm gonna create it,
give it an opacity of zero, add it to the document, and then
transition it to full opacity. Cool. Yeah, cool? DAS SURMA: Eh, no. JAKE ARCHIBALD: Go
on, tell me about it. DAS SURMA: The
only reason I know this is because we, actually,
had a question about this, I think, on the first
[INAUDIBLE] quiz at some point. JAKE ARCHIBALD: Yeah. DAS SURMA: And then
we started testing, and discovered that browsers
were still behaving differently here, actually. So I think because
this is all JavaScript, there is no yielding to the
browser between the append and the subsequent
changes of the styles. So the browser [INAUDIBLE]
the rendering engine only sees the transition
and an opacity of one, because the previous change
is invisible to the rendering engine. JAKE ARCHIBALD: Spot on. Yes, absolutely. The browser never
sees the opacity zero, it never thinks about
it in terms of styling. It's just a JavaScript
value, and it's overwritten before it thinks about
styling anything. So the way you work
around this is either by waiting until the next
frame, which means using request animation frame twice. Of course it's twice, because
request animation frame is before the next frame. So if you want to push
into the next frame, you have to use it twice. Or you hack around it. So all I'm doing here is
using getComputedStyle, and just calling
getComputedStyle is not enough. You have to then read
a property from it, and that is when the browser
will think about the style. And it thinks about
that opacity zero, and then it will
transition to one. Fine, OK. Let's fade something out. There we go. Pretty good. Good start. But what if like
opposite to before, I want to remove it
once it's faded out. So let's give that a go. You get an event. So there we go, job done. Can you see the gotcha here? DAS SURMA: Actually, I cannot. JAKE ARCHIBALD: And neither
did I for a long time, until it caught me out. And it's that events bubble. So I could get this
event, not for when my transition finishes, but from
when some other thing finishes its transition. And once you start getting
into more complex transitions where something inside
fades out slightly sooner, you start finding your things
disappearing from the document much sooner because you're
actually catching that event from another animation. DAS SURMA: So you
should check for-- yes, OK. JAKE ARCHIBALD: So that
caught me out as well. But if that element was
already zero opacity, you get no transition. So you don't get an
event, which is great. So now if you're building
a more generic animation system, that's something
you have to check. Because people will say,
make this go to zero and then remove it. And if it's already zero, they
just want it to be removed. This kind of code is
extremely fragile, because you'll be checking
what the input value versus the computed value. But even with opacity, 0%
and 0 are the same thing. The computed value will
always come out as zero, but the user of the library
might have given you 0%. You're not going to get a
transition from those two because they compute
to the same value. And we don't have the tools to
easily convert arbitrary values into computed values. Also, if it's display
none or it's inside an element that's
displaying none or it's not in the
document, you're not going to get a
transition event. So this is just not
going to work properly. So we'll throw all of
that out, and start again. What we're going to do this
time is use CSS animations, because this arrives slightly
later than transitions, but we've had them for at
least a decade now I'd say. This just works. So I'm setting the
opacity, and then it's animating from opacity zero
to it's computed style, which is going to be one. And I don't have to do
any computed style hacks. This will just work,
which is great. DAS SURMA: Yeah, this is
my current go to preference for making things appear when
they get added to the DOM. Just like, it
automatically plays once, it stops at the end. It's brilliant. JAKE ARCHIBALD: Which
is great, isn't it? Because we are
now in a situation where it's easier to use CSS
animations for transitions than CSS transitions. Great. Excellent. But yes, I agree with you. This is moving towards how
I do things with animations these days. So what about the
fade out and remove? That's a slightly
different model. So I'm going to add
another keyframe animation, this time fading out. And off we go. We still need to deal
with the bubbling, still have the event
bubbling issue, but this time it's fine
if the value's the same. So if you're animating from
opacity zero to opacity zero, you'll still get the
event, which is great. DAS SURMA: Oh really? JAKE ARCHIBALD: Yes. DAS SURMA: I did not know that. JAKE ARCHIBALD: You won't get it
if it's display none or inside an element that's display none. So we're making steps forward,
but we're not there yet. But then, enter the
Web Animations API. This just works, which is great. So I'm providing my
one and only keyframe, which is opacity zero. And I give it an
offset of zero, which means that this keyframe is
at the start of the animation. The offset between zero and
one, and off it will animate. It will animate
from opacity zero to whatever the
style resolves as, which would be one by default,
but it could be something else. So this works in
those cases as well. DAS SURMA: Does this
currently working Chrome, this exact syntax? JAKE ARCHIBALD: Yes it does. Yeah, so what you're thinking
there is-- and for a long time you had to specify at
least two keyframes. DAS SURMA: Yeah. JAKE ARCHIBALD:
You don't anymore. This just works. It will figure out the other one
automatically, which is great. I wish it was there
from the start. It wasn't, but yeah,
this works now. And it's well supported,
the Web Animation API-- if you're targeting modern
browsers-- it's everywhere. It arrived to Safari more
recently than the others, but it's there now. So this is how I
do animations now. DAS SURMA: Well, it shows that
Chrome support of Animation API from 44 to 84, but I think that
wasn't complete support, right? So that table seems to
be a tiny bit misleading. JAKE ARCHIBALD: That's true. So in terms of the
multiple keyframe thing, I think it was actually
Firefox that landed that first. So credit to them, but yeah,
Chrome supports it now. So yes, although that chart
shows everything looking good for a long time
in Chrome, there are features that I'm
showing today that arrived in between then and now. And I didn't look up
when, so don't ask me. But they've been in Chrome
for multiple versions now, so they're safe to use. So what about fading out? There you go, that's it. JavaScript animations,
they finish even if it's display none or
if it's not in the document, so you don't have to
overcome that as well. Actually, there is a
bug here in Chrome. DAS SURMA: Of course there is. JAKE ARCHIBALD: Yeah. Bloody Chrome,
spoiling all our fun. The promise resolves too late. So the animation will finish,
and then sometime later the promise resolves, which if
you're doing accurate animation work, this is far too late. So instead of the
promise, use the events. This is fixed in Canary, so
it's on its way to stable. But yeah, that's a
workaround that works today. I want to talk
about more gotchas. What if I wanted
to fade something from whatever it is now to not
point to opacity, and leave it. I'm not going to remove
it from the document. Something like this. Can you see the gotcha? DAS SURMA: Um. I mean, I'm immediately
drawn to just one keyframe where, I don't know
if it's implicit that it's at the
end of the animation if you just specify
one keyframe. JAKE ARCHIBALD: That's
a good point, yes it is. But if you provide
one keyframe, it implies the end keyframe
for some reason. DAS SURMA: OK. JAKE ARCHIBALD: So
we gotcha here-- DAS SURMA: Then I don't see it. JAKE ARCHIBALD: Yeah. And I mean, if you ran this code
you would see it immediately, because here's what it does. Boop. The animation ends, and
then the animation is gone. So everything that the
animation was affecting, stops being affected. DAS SURMA: Yeah,
that's why I usually-- I almost always define
the fill behavior because I just don't
trust the defaults. JAKE ARCHIBALD: There
we go, job done. Fill forwards. And, yep, now it works. DAS SURMA: So just
to be clear, that means when the animation
ends, and in the timeline you have keyframes, and you
play from one key to the next. Fill tells you what happens when
the time is outside these two keyframes. Should you loop? Should you fill? Or there's probably
others as well, but yeah. That's what this probably does. JAKE ARCHIBALD:
Looping is different. It's forwards and backwards
in both of the values. So yes, if you have an
animation that is delayed, a fill of backwards means it
will apply the first keyframe while it's waiting for
the animation to start. A fill of forwards means that
once the animation is done, it will hold the last state. Both does both. But I have come
to the conclusion that this is a huge gotcha. And every time I have done this
it has bitten me at some point later on, especially
if I'm doing something a little bit more complicated. And because of this. It doesn't work. The animation has
higher priority. DAS SURMA: Oh, because the
animation is technically still running, I guess? JAKE ARCHIBALD: Effectively
it's still held. And while an
animation is running or it's still happening,
to some extent, it has priority over
almost everything. The only thing it doesn't
have priority over is important styles-- when you do
exclamation important-- it doesn't have
priority over that. But yes, if your solution
is to use important styles, you've done something
wrong earlier on, right. So we need to fix
this some other way. And, actually, there's
a method, which I'm going to struggle
to remember, like, document.getAnimations. And you will see all of
these animations that have fill forwards. Anything that ever
ran, essentially, is going to be in there. DAS SURMA: Oh, I
didn't know that one. That's neat. JAKE ARCHIBALD: Yes. Because in CSS you will
remove that animation by removing the style
resolution, which has the animation property on it. So if you remove
that class name, then it removes the animation. With Web Animations, you
need to deliberately cancel the animation if
you wanted that. So what's the answer? And it's taken me
years to figure out, and so I might still be wrong. But here's the conclusion that
I've come to, so strap in. DAS SURMA: Just
don't animate, ever. JAKE ARCHIBALD: Don't
animate, it's awful. People hate it. No, I'm going to make an
animateTo function, which takes the element and
then the [INAUDIBLE] animation arguments, the
keyframes and the options. I'm going to create the
animation and return it. So this will run to
completion, and then snap back to the start
values as we saw before. So we're going to fix that. I'm going to create
a finish event. I could use a promise,
except for Chrome, that bug. So I'm going to use the
events, and then I'm going to call commitStyles. And this is an API that not
many folks seem to know about. What it does is it commits
inline styles for the animation at its current position. So you can call this multiple
times in the animation and it will just dump
out in line styles for whatever's
happening right now. There's a problem here, though,
because the current position is finished, so
everything has already snapped back to the start. So this doesn't work. There is a spec disagreement
on this, already, that maybe that shouldn't
be the behavior. Maybe this should just work. And if you call this
after it's finished, it will give you
the final styles. But that's not the
way browsers behave. So I'm going to add fill both--
and I know I said that was bad, and it is, but we need it
so we have those styles when the animation ends. And then once committed,
cancel the animation, which removes it from the que. And you end up with
the inline, so it's held the final
position, but it means that you can update
inline styles, like run, and it will all work. DAS SURMA: And for the
people already wondering, I'm pretty sure this event
listener doesn't leak. JAKE ARCHIBALD: No,
because it fires once and then the browser
knows it's not going to be fired again,
especially once the animation is garbage collected. So the animation
can't be restarted. DAS SURMA: Because animation
can get garbage collected, yeah. JAKE ARCHIBALD: Yeah, I
guess that the listener could leak if you were holding
onto the animation object, because you could
restart the animation. And in which case, you
could get finish again. But yeah, it's otherwise fine. It's not something
to worry about. My previous solution to this
was to apply the inline styles manually. And I wouldn't use fill
at all, because I hate it. But this raises a question
about composited animations that I've seen browser
folk argue about. So if you've got an
animation that completes, you get your finish event, and
then you set the inline styles. What if your main thread
is blocked at that point, and the animation is
happening on the compositor. Some people would argue that
the animation would complete, snap back to the
original values, and then some time later
you get your event. So you would still
have a visual flicker. Like, animation
completes, it snaps back, and then you apply
the inline styles. I couldn't get any browser
to actually do this. So it seems like it's a bug
they've solved or worked around it some way, but that
is not in the spec at all. So maybe that will
change in future. DAS SURMA: I mean,
it would make sense to somehow tie the value
that the animation object is representing to the time that
the event is being processed, or the event is created
and put in the event que. But yeah, that
definitely is something that needs to be specified
and written down that that is how it's supposed to behave. JAKE ARCHIBALD: Oh, and even
if the values are synchronized, it's more about what you're
seeing on the screen that needs to be synchronized. And what it seems
like browsers do, is they run the animation
on the compositor, so off the main thread. But then the removal
of the animation, where it would snap back to
the start point, that is still handled on the main thread. So that means that you get
your event in synchronization with that thing happening. But there's no spec for
how the composer works. But browsers seem to be
doing the right thing. But to defend against
changes, in terms of how the compositor
works with the main thread. This code will
defend against that, because it's going to
use that fill forwards, or fill both in this case,
to hold that final frame until your JavaScript
gets to run. And it's your JavaScript
which removes the animation from the que. There you go, like, 20 years
it's taken me to get to this. A combination of my
own learning, and also features being added
to the browsers. DAS SURMA: You had to use
fill in the end, after all. JAKE ARCHIBALD:
After all, but there is discussions in the spec
to avoid having to do this. Where if you call commitStyles
at the end in the finish event, it will apply the final styles,
which that's just what I want. That's what I always wanted. Just make that
easy for me please. But in the meantime,
I at least now have a simple small function
that does exactly what I want. And that's all I've got. DAS SURMA: Having fiddled
with writing an animation "engine" which makes it
sounds way fancier than it was in procs. I appreciate the simplicity
of what you boiled it down to. So put it in a gist, I
might want to copy it. JAKE ARCHIBALD:
Yes, I'll link to it in the description, along with
the various bugs, and spec, bits, and arguments, and
stuff that are happening. And I'll put that down
in the description. [INAUDIBLE] Did you clap that second time? DAS SURMA: Yeah. JAKE ARCHIBALD: Oh, OK. DAS SURMA: It's just
perfectly in sync. JAKE ARCHIBALD: My earphones-- DAS SURMA: Or maybe
the sound-- well, maybe the GVC swallowed it. JAKE ARCHIBALD: Yeah, probably. DAS SURMA: I could
see that happening.