@Eunomiac Sooo I finally got around to
@Eunomiac Sooo I finally got around to playing with GSAP, and it looks incredible.
I'm running into one issue though that I can't find a clear answer for, perhaps you know!
TL;DR is it possible to crossfade an App re-render?
I have basically all my (AppV2) apps set-up using Handlebars rendering; so whenever the app re-renders the DOM is fully replaced. I looked into using Flip but couldn't get that to do much of anything (as it appears to mostly be 'positioning' based, so f.e. if only text or only images change, but not the position of their elements, I can't get it to cross-fade that transition).
I tried creating a cloned element set to absolute positioning over the current app -> re-render the app -> fade out the old app while fading in the new one, but this "stutters" (primarily due to images, see attached example pre vs post
appendChild
call of the cloned dom element) .
Don't really have any other ideas to try and my google searches are failing me, so wonder if you have any ideas, or how you handle this in your use of gsap, or; if this is an issue you never run into because what I'm trying to do is silly and there's a much easier way to do stuff ^^;

94 Replies
Opening up a thread to answer to the replies...
@Ethaks
Flip
should work across handlebars renders as well, although you might need to give GSAP a little support there (including getting the state pre-render and applying it on render, possibly using flip ids to track things).
Yeah so what I was effectively doing was :
And I had set data-flip-id="foo"
in the template.
@joaquinp98
exactly which apps do you want to crossfade? or do you wanna crossfade a element on the app?I've tried a single element and a PART, neither with any success.
Without knowing all the details, some of the knobs closest to hand usually are the selectors used, as well as some of the options like
absolute
, absoluteOnLeave
, and nested
.But in theory it should be able to do this? cause as far as I could see the Flip was doing absolutely nothing; so I stopped trying to play around with it.
While I am far from a GSAP/Flip guru, I'd guess yes.
I mostly use Flip to animate e.g. items in item lists so that e.g. toggling between alphabetical and manual sort mode animates etc.
But that does mean that such animations are run across hbs renders, which works mostly fine on my end.
Due to my own missing experience with GSAP it took quite a bit of throwing paint at the wall though ๐
I've got a "Token Dashboard" that presents different options depending on the token selected which I tinkered onto the Hotbar (macro bar).
I'm trying to animate it cross-fading from the old token's state to the new token's whenever you select a new token.
PARTs at the end of the day are still only elements when the app is rendered
yup.
my advice is to perform the entire animation before re-rendering.
some like
Click on Button -> Element-1 fade out -> Element-2 fade-in ->re-render the app
I mean effectively to be able to do that, I'd have to render the app twice, not?
It'd be Click Button -> Current app fades out -> manually prepare context & render the template HTML -> render this manually prepared version in -> fade it in -> re-render using 'natural' AppV2 render flow.
Which... would be very counter productive.
Tried the above with absolute & nested on true but still no animation at all.
Despite the duration of 1.5 it seems to also just instantly finish
actually, you only have to render the part with the context, nothing more complex is needed
and this is done in one line
You have any example? Cause I don't think I'm following what you're putting down
Ah I might've figured it out,
Flip.from
seems to have been missing targets
.I don't know exactly how Flip works but I could use something like, but it's probably better to use a timeline.
this is just a crude example
I repeat it would be better to use timeline instead of doing it this way, but I don't remember well how to do it, I should review it.
oh i find a example what use Flip....
Yeah but in this example effectively what is being written is
const newEl = this.render()
minus the "replace current dom" part, then we animate said transfer, and then we render again and just replace the dom with an exact copy of itself...?use
this.render
is not the same as renderTemplate
the first one involves updating the listener, calling the hooks, performing all the preRender, onRender, replace the app entirelyYes but to re-render a part to the proper state I'm gonna need to do all of those things too?
I mean perhaps not activating listeners.
But everything else I do.
so you still need to store a variable that keeps between re-renders the current state so that when you re-render it stays as is but that is not done with animation
What I've got now that actually animates something is:
Haven't tested if absolute/nested are necessary, the only problem with this is that while it fades in the new element; the old one isn't faded out, presumably since the Handlebars renderer has already removed them from the dom
exactly how the animation is
I don't know much about Flip, if I had to do it, I would use timeline.
but I don't understand why to trig the animation in the _onRender
I don't understand the existence of the rerender in this example either
Where else would I?
I repeat, I do not understand what animation you are trying to achieve.
for example let's say you want to replace one image with another when a user clicks on a button then trigger the animation when the user clicks on the button.
I assume that the animation is a reaction to an input from the user.
or do you want to set an initial animation in the app?
So I explained above that my goal is that this is a "Token Dashboard". Whenever you switch control of a Token the application re-renders to update its information for the new token.
So in this case the 'selecting of a Token' is the equivalent of your 'pressing a button'.
What I want to do is a crossfade from the old state (Token A's Dashbaord) to the new state (Token B's Dashboard).
Both with
Flip.from
as well as just using a timeline, I can in the _onRender
get the element to 'fade in'.
But I can't get the crossfade to work.then I would create a method called: โonChangeTokenโ and call that method when changing tokens.
Pressumably because the AppV2 render call replaces the dom.
The token interactivity is sorted.
The "button" works.
the cross-fade animation doesn't.
it only fades in; not out
the rerender method is the method you call when another token is clicked?
Yes.
so i do this
you will probably use the currentState somewhere in the rendering cycle so that the app has the correct state.
and your
myMethosForMakeTheAnimation
would require calling _prepare(Part)Context
and the like to prepare the context to feed to renderTemplate
to get the "new" state as part of your animation before you call this.render
, so you're effectively calling this.render
twice?the
render
method of an app is much more complex
and the context can be obtained in a simple way
think that when you call renderTemplate it is actually rendering a string without listeners, without actually existing in memory.
and avoids the hassle of dealing with the entire rendering cycle to fix that.I can't get this approach to function at all FYI.
mmm... well the example I found it on a forum page so it may be terribly outdated
or I may have made a mistake when adapting the example
should do some serious testing with Flip
This 'works' but isn't a cross-fade.
if I change it from
onComplete
to onStart
I get the same issue where it only fades in, not out.mmm... I will see if later I can make a simple example in foundry.
AppV2 has two methods for sync'ing rendered PARTS, mainly used to store input focus and scroll positions. I've used it successfully to animate health bars and fade in and out various elements across re-renders.
So would you imagine then to take the flip state in
_preSyncPartState
, and then in _syncPartState
you run the Flip.from
? cause I imagine that'd just result in the whole "the new element fades in but the old one doesn't fade out" issue.I would fade out -> submit (causes re-render) -> fade in on render
But I did not look too closely at the sheet in question here. ๐
Ah see that is easy, but the issue is I've been trying to get it to cross-fade.
I'll scroll up and get a better idea but what does cross-fade mean in this context?
The fade in & out occur simultaneously instead of staggered
Ah, so for example two sheets and the old fades out, the new fades in, all at the same time
Yup.
I believe changing token art these days does the same thing
Yes, it does.
I think that is PIXI, not animated html tho.
Oh yeah no correct; I more so meant as a visual example of what I'm trying to achieve
So we're on the same page x)
I wonder if overriding
HandlebarsApplicationMixin#_replaceHTML
is helpfulI thought about it but I don't see how it'd end up being any different, I'm starting to believe that cross-fade is just not a possibility within HTML + GSAP.
At the very least; not while replacing DOM elements.
Yeah I'm not much help, sorry. My only ideas would be to, somehow, cause the replaced HTML element to fade out in its current position instead of just being fully replaced (in
_replaceHTML
), or duplicate it, re-insert it, and manually fade it out and remove it in _syncPartState
.So far the thing that works best is:
for some reason depending on your screen size the fade jumps by like 0.7px...
But besides that it works
It also 'blinks' (e.g. there is one frame where the "old state" is removed, then it comes back in the next frame, before being faded out)
@Eunomiac I think your last suggestion in #development is basically this xD
Ah yes, the FOUC -- "flash of unstyled content". This is why it helps to use your CSS styles to set whatever you're animating to
visibility: hidden
, so that it's invisible until you animate it with GSAP via autoAlpha
(which works the same as animating opacity
, but it animates out of visibility: hidden
)
That way, when it "blinks" (i.e. the frame before GSAP gets ahold of it), it'll be invisible at that time so it'll look a lot smoother.How'd you edit the above to achieve that?
like just put visibility: hidden in the gsap.set
and then autoAlpha: true in
.to
?I'd do it in the CSS styles. I'd set the whole
this.element
to visibility: hidden
in CSS.I mean that's what gsap.set is doing (or well it's setting it on the
style
but I assume that shouldn't matter?)And then go
gsap.to(this.element, {autoAlpha: 1, duration: 0.5, onComplete...)
The problem is that flash-of-unstyled-content is appearing in the brief frame between when the DOM loads, and when GSAP is able to take control of the elements' appearances. By setting the element to visibility: hidden
in your .css
file, that applies as the DOM is loading, so you don't see the flashAh to be clear, this flash is happening way-past dom load
I'm triggering it with "a button click" (effectively)
(selecting a token)
Err, it's unrelated to a server-triggered re-rendering of the DOM? Okay, that's a whole different issue. Could you explain it in more detail (or point me to a message where you already did?)
The full code is effectively this above + in
Token._onControl
I go: app.token = this; app.rerender();
Generally speaking though, a flash like the one you're describing is because the element is being displayed before GSAP has a chance to set it to
opacity: 0
. If you're adding a cloned element to the DOM, the same thing will apply. The fix is to make the element invisible until GSAP has a chance to manipulate it.Hmm, I've now right after
const current = this.element.cloneNode(true)
added currentState.style.visibility = "hidden";
and it's still flashing .-.No, what you want to do is make sure the
visibility: hidden
is set in the CSS styling of the element --- you can't rely on GSAP to do it, or Javascript for that matter, because Javascript isn't "fast enough" to catch the element immediately.But... JS is... creating the element... I set visibility to hidden before it is added to the DOM... how can it be too slow :psyduck: :LaughingSor:
What I can see occuring if I put breakpoints:
Before I create the timeline the
new
state is fully rendered while the old state is fully unrendered
, which is what is causing the blink.
So yeah this is the whole "I need to stop AppV2 from actually rendering and replacing the dom and let gsap do it completely"I'm embarrassed to say I've been using JQuery for so long, I've forgotten what most of the native DOM methods do, lol.
Oh! That might be why GSAP isn't able to do anything with it. In fact I think that's it --- GSAP doesn't style elements well if they aren't in the DOM. You could try setting the
cloneNode
creates the element unattached to the DOM entirely?Oh! That might be why GSAP isn't able to do anything with it. In fact I think that's it --- GSAP doesn't style elements well if they aren't in the DOM. You could try setting the
style
attribute using native DOM-manipulation methods instead of gsap.set
, that might work. (In JQuery, it'd be $(current).attr("style", "visibility: hidden")
.
Before I create the timeline the new
state is fully rendered
Yeah. To fix this, you need to make sure that the style of the new
state is visibility: hidden
--- not try to set it via Javascript or GSAP, but in the baseline CSS. Then, you animate autoAlpha
to 1
to reveal it. Or even gsap.set(this.element, {autoAlpha: 1})
when you're ready for it to be shown, if you aren't fading it in.I tried that actually, and because the result was the same I decided to move it over into the GSAP set because it's shorter to write, it didn't make a difference x)
Try doing it with a class assignment. Give it a class like
pre-hidden
as your create it/before you attach it to the DOM, and then in your .css
file, have .pre-hidden { visibility: hidden; }
.
Then test it without GSAP at all --- you simply shouldn't see it at all, no blink no nothing. Then you can be sure it won't appear until GSAP is good and ready to make it appear ๐
(That's actually kind of key to this discussion --- you should be able to get rid of the blink without using GSAP at all: If you can get it so that it renders "invisibly" without blinking, then you can use GSAP to animate it in)
But if you use CSS to style the component visibility: hidden
, there's no way it should appear until you make it appear via GSAP (or by removing that class/style property, etc)I... feel like autoAlpha1 is setting visibility to hidden once it's done
could that be correct o_O?
You can also use a pseudoselector, if the blink happens while both elements are present in the DOM --- i.e. if you attach your cloned element after the original one before removing the original one, a selector like
.myApp + .myApp {visibility: hidden}
will ensure that any element with the class myApp
that appears after another myApp
element is hidden
It should set it to visibility: hidden
if you animate autoAlpha
to 0
, definitely not the other way aroundyeah no this just isn't working.
Tomorrow imma try again but with GSAP taking full rendering control instead of re-rendering with handlebars & then trying to animate the transition.
Thanks for the advice tho!
That would be my advice, for sure --- but the answer is almost certainly going to be setting the element to
visibility: hidden
in your .css
file, I'd bet my mother's life on it.
Wait, that's not how the saying goesI did that ;-;
It doesn't blink but it also doesn't fade out anymore now x)
So, despite the element having a CSS-set style of
visibility: hidden
, it still flashes? That's bizarre. And it shouldn't have anything to do with GSAP --- i.e. if you remove all of the GSAP (or comment it out briefly ot test), you should still see that flashExactly. And that would confirm that the styles and DOM manipulation are working properly --- if you see nothing, no flash, nothing at all, then you know that you've correctly rendered the element to the DOM invisibly, so it's ready for GSAP to animate it back in.
Once you confirm that, add back in the GSAP. If the flash reappears, then yeah, GSAP is doing something weird.
You can also try using
You can also try using
fromTo
instead of from
-- I find it's more resilient because you're setting the start and end points. E.g. gsap.fromTo(this.element, {autoAlpha: 0}, {autoAlpha: 1, duration: 0.5})
I might've cracked it... lets seee....
nope, this way there is just no fade out happening :/
This, the fade-in, works 100% totally fine.
This, the fade-out, blinks into existence
before fading out
It might be that
this.oldState
isn't resolving to the element. Try using a selector, or re-fetch the element right after that await super._onRender
call
Oh, so it is resolving. Try using fromTo
wherever you can --- tl.fromTo(this.oldState, {autoAlpha: 1}, {autoAlpha: 0, duration: 0.5} ...
And don't use both opacity and autoAlpha --- autoAlpha handles opacity and visibility, so it's redundant to include opacity as well. (That might actually be causing the problem, I'm not sure)
(Changed opacity
to autoAlpha
in my above example, just FYI ๐ )
Also I just want to reiterate that I've never liked gsap.from
--- I always seem to run into problems with it. I exclusively use to
and fromTo
.
Especially when building timelines.OOOkay I have it 99% now I think
My issue
Was it the use of both
autoAlpha
and opacity
that was causing the problem? That would be my suspicion.was that I was appending the oldState to
this.element
instead of this.element.parentNode
Ahhhhh, that makes sense -- so
oldState
was being animated out and in at the same timefor some reason
this.oldState
is again like 0.7px off.
yup0.7px off position-wise? Set
font-size: 0
in the parent element -- sometimes a simple white-space can cause thatIf you overlay these two images in any kind of photo editor with layers you can see what I mean.
font-size: 0
on the parent doesn't seem to fix it :(

Hmm. What does the style inspector in the dev console show? Any differences in the box positioning?
Well the "pre" isn't positioned (in the sense it ain't absolute)
I swear it's that .594

That might be why -- if you're trying to align two differently-positioned elements, that can be tricky, especially if some of their dimensions are floats -- like that 174.594
Maybe consider a parent wrapper element set to
position: relative
, and then set both the pre and post positions via position: absolute
?hmmm interestingly enough, setting position relative on the parent and position absolute just works.
Like the element doesn't move and just stays exactly where I want it to, never had that be the case before lol.
But if I do that (and then also don't set any top/left/width/height on the clone) they indeed become identical
to this

Ahh, I see -- the element is being centered horizontally in the second case, which was causing that 0.3px offset
Yeah ideally you want the DOM structure (including the stacking context, set via
position: relative
on the parent) to be identical for both of the elements you want to align.oh no how did the stutter return now aaaagh.
And you should be able to position both elements without misalignment if they're both children of a
position: relative
parent -- and assuming you set their positions identically, of course.I don't understand why the flicker is back gah.
I think this may have to do with the fact that images are refreshing
so it feels like it's blinking but in actuality only the images are
okay updating rerender to this fixed it:
Fully working cross-fade while keeping the re-render with Handlebars, woop!
fun fact; I don't need the css visibility: hidden rule anymore either now x)
The full diff that got it all working x)

Thanks btw @Eunomiac! Now that I've got a bit better of a grasp & it's working it's a lot more fun to play around with :ashelaugh:
For tonight I'm done, tomorrow it's time to animate more stuff & things~

@Eon gave :vote: LeaguePointsโข to @Eunomiac (#77 โข 30)
Maybe figure out a mixin for adding gsap animation of handlebars parts :sprigatitoNote:
Oh that's nice! I've actually never done much with the canvas --- in fact, in my current system, I
display: none
it completely, lol. Everything is apps and popups.
One thing you might find really fun to add to your system and animate with GSAP are alert popups for when fun things happen --- I had a blast making these ๐
But this is probably my fave of the things I've done with GSAP
