Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transitions #7

Closed
Rich-Harris opened this issue Nov 21, 2016 · 7 comments
Closed

Transitions #7

Rich-Harris opened this issue Nov 21, 2016 · 7 comments

Comments

@Rich-Harris
Copy link
Member

A UI library needs a transition system. Ideally it would have the following features:

  • transition code shouldn't be deeply intertwined with the rest of the code (as in Ractive)
  • should facilitate JS or CSS transitions
  • should have zero cost for people who aren't using transitions
  • transition plugins should be easy to write
  • control over order (e.g. Vue has out-in, which is nice – could get even more granular for staggered transitions)
  • nice-to-have – abortable/reversable transitions (e.g. if something is fading out because you toggled visible, and you toggle it again before the fade completes, it'd be great to unfade the same element, rather than letting the fade complete and rendering a new element)
  • some way to access the transitions in such a way that you can do advanced stuff like transitioning one element into another, a la Ramjet

Proposed syntax, following that used by existing directives:

{{#if visible}}
  <div in:fade='{duration:200}' out:fly='{x:100,easing:'elastic'}'>hello!</div>
{{/if}}
@Rich-Harris Rich-Harris mentioned this issue Dec 15, 2016
@proyb6
Copy link

proyb6 commented Dec 16, 2016

Does that including transpile CSS2/3 syntax with dynamic value to Javascript on SSR or client side?

Ramjet was nicely done for complex animation!

@evs-chris
Copy link
Contributor

evs-chris commented Apr 19, 2017

I've been pondering transitions for a while now, and I'm starting to come around to @martypdx's view that transitions are more an extension of the eventing system than their own standalone construct. An intro is just a sort of stepped decorator transform that you want to apply to a node immediately after it is attached to the DOM, and an outro is the same but to be applied immediately before it is detached. If you can achieve an on:mount/on:unmount set of hooks per element where the unmount saves DOM detachment until a returned Promise resolves, I don't think you could get much simpler.

Once you have a good starting point, you can throw sugar at it, probably in the form of compiler plugins, though the penalties for more code in the svelte compiler are significantly less far-reaching than those in a runtimier library.

The tricky bit is coordinating when to transition or remove a particular node, depending on whether it, an ancestor, or a child have a transition(s) or should transition if they have ancestors transitioning. There are scenarios in which you would want to have a child skip its transitions because an ancestor is transitioning and you don't want the child to slide about while it's group is fading out.

I don't really see a way to do abortable or reversible transitions while still keeping the mechanism for transitioning fairly open ended (css transition, css animation, js, ... something else?). The safest way to achieve that would probably be to expose the hook queue of the transitioning node to the listeners. Then a (probably js-only for sanity) plugin could do state-based transitions and handle stopping the in-flight transitions and setting up the state for its starting point.

@Rich-Harris
Copy link
Member Author

I actually fell asleep thinking about reversible transitions last night (I know, I'm weird). There's definitely a tension between the state-driven paradigm that Svelte, Ractive, React, Vue et al represent and the event-driven paradigm (jQuery, Backbone etc) it replaces, and transitions straddle the two awkwardly.

So I was wondering if there's a way to reconcile them. I haven't sat down and properly thought this through, but it occurred to me that if a 'transition' is just a function that returns a function that takes a single argument t (the progress through the transition) then maybe reversible transitions are possible:

{{#if visible}}
  <p transition:fade>now you see me...</p>
{{/if}}

<script>
  export default {
    transitions: {
      fade ( node ) { // or `( node, params ) => {...}` as appropriate — see below
        return t => node.style.opacity = t;
      }
    }
  };
</script>

That's a terrible example because it should be a CSS transition (which I'll come to) but you get the idea — rather than the transition function taking care of business and saying 'hey Svelte, I'm done' once it finishes, Svelte is driving the whole thing. So if the element is 95% faded out (i.e. t === 0.05) and visible becomes true once again, there's no need for any sort of cancellable Promise hackery or anything like that.

For CSS transitions, you'd basically just be specifying the start and end states, which is effectively the same as letting the browser create the t => [style] function.

Coordination between transitions (chaining, skipping transitions for children of transitioning parents, etc) is definitely not a straightforward problem, nor is figuring out when an element can be detached. I wondered if maybe the detaching problem could be solved like so:

{{#if visible}}
  <p detachgroup:foo>
    this will remain in the DOM until all nodes in this detachgroup have outro'd
  <p>

  <p outro:fade='{duration: 2000}' detachgroup:foo>
    this will fade out over two seconds
  </p>

  <p>
    this will detach immediately when `visible` becomes false, because
    it's not part of any detachgroup
  </p>
{{/if}}

(detachgroup isn't a particularly elegant name, I'll confess. outgroup?)

Chaining could maybe be done like this:

{{#if visible}}
  <p outro:fade='{priority: 2}'>
    this will fade out once the other <p> fade out is complete
  <p>

  <p outro:fade='{priority: 1}'>
    this will fade out first
  </p>
{{/if}}

Or maybe something like this:

{{#if visible}}
  <p outro:fade='{start: "foo.end"}'>
    this will fade out once the other <p> fade out is complete
  <p>

  <p outro:fade='{name: "foo"}'>
    this will fade out first
  </p>
{{/if}}

One possible answer to the problem of transitions-in-transitions:

{{#if a}}
  <div outro:fly>
    <div outro:slide>this will slide out while flying out</div>
  </div>

  {{#if b}}
    <div outro:spin>
      this won't do anything if `b` becomes falsy
      at the same time as `a`
    </div>
  {{/if}}
{{/if}}

Maybe you'd want more control than that, I don't know.

Finally, we'd want to have some control over the transition. If we did the t => [style] thing, then some parameters would be for Svelte to worry about (duration, delay, easing etc) but some would be specific to the transition function itself. So I guess we'd need to do one of these:

<div transition:foo='{delay:500}' transitionparams:'{scale:2}'>...</div>
<div transition:foo='{delay:500, params: {scale: 2}}'>...</div>
<div transition:foo='{delay:500}, {scale: 2}'>...</div>
<div transition:foo='{scale: 2}, {delay:500}'>...</div>

Feels good to try and articulate some of this stuff! Implementing any of it would probably feel less good...

@evs-chris
Copy link
Contributor

I actually fell asleep thinking about reversible transitions last night (I know, I'm weird).

Nah, that's perfectly normal 😃.

I wondered if maybe the detaching problem could be solved like so:

Named grouping would be an interesting approach to detachment. It might get hairy with component boundaries and runtime tracking, which seems to be core to the issue of transitions in svelte. We're trying to avoid runtime wherever it can be, and transitions are inherently runtimey (at least they are to me). Given that and that you don't usually won't a ton of animations kicking off at the same time while, for instance, changing page routes, perhaps it would be better to simply limit transitioning to non-nested nodes (meaning no transitions within transitions) in the topmost transitioning component? The compiler can probably manage a bitmap of some sort for tracking transitioning nodes pretty safely/easily, but component boundaries would probably become a problem if you try to allow crossing them. It probably doesn't matter so much for intros.

(Probably talking to myself for this, as I'm pretty sure you're already thought/dreamed through it much more thoroughly) Since all non-EOL browsers appear to support CSS transitions, I suppose you could base transitions entirely on those. Pass the whole transition params object into the transition function to get back the start and end state and allow it to add custom default duration, delay, etc to the params object if it so desires. Then implementing the transition becomes: record current property values based on the start state, set the initial properties from the start state, install an appropriate inline transition rule, set the end properties from the end state, somehow watch for interruption (property/function on the node? local flag var, since svelte has the technology?), and add a transitionend listener, if it will actually fire (probably set a duration timeout too, just in case). If the transition is interrupted, kill the timer and listener, re-apply the start state, and install a new listener and timer. When the end happens, kill the timer and listener and apply the initial state.

It seems like transition sequences would be pretty reasonable to implement just by reading additional directives: <div in:grow in:fade out:fade out:shrink>...</div>, which would gather the states for all specified transitions, apply all of the initial states together, and then apply each successive end state as prior transitions completed, adjusting the duration and delay on the inline transition rule as needed along the way. I'd say that parallel transitions would be better left to combined transitions (in:growAndFade) or perhaps even a svelte built-in e.g. <div in:style="{ opacity: 0; height: '0px' }">...</div> where the second state is implied to be the initial state of the element (for intro, the end state is implicit, and for outro, the start state is implicit). Actually, thinking about it, I think that style transition would cover 95% of my transition use already.

That would certainly leave out ramjet-like things, but perhaps that's where transitions as an event hook/decorator comes back into play?

@Rich-Harris
Copy link
Member Author

Ack, component boundaries! Didn't think of that. Certainly does make things harder, or at least limit our ability to write the code ahead of time.

If components emitted transition events (which I think they would have to in any case) then I suppose you could do this:

<Widget on:foo.end='destroy()' visible='{{bar}}'/>

<!-- equivalent to this, if this was possible -->
{{#if bar}}
  <Widget out:fade='{name:"foo"}/>
{{/if}}

(I'm imagining that setting visible to false would trigger a foo.start event followed by a foo.end one, because of name:"foo". Perhaps we would also have transition.end and outro.start and whatever else — details to ponder another time.)

I love the idea of in:style='{...}', particularly it being built in. That would indeed meet 95% of situations. Would like to support the other 5% though as it includes nifty stuff like SVG stroke-dasharray hacks and typewriter effects, which are a guilty pleasure of mine. I'd probably use ramjet-style effects a lot more if it wasn't such a PITA as well.

Given all the headaches I've experienced with transitionend events, I was wondering if it would be possible to do all this just with timers — i.e. set a 2 second transition off, and assume it's completed when the first requestAnimationFrame callback happens that's more than 2000 milliseconds in the future. Don't know how bulletproof that is.

Rich-Harris added a commit that referenced this issue Apr 25, 2017
Rich-Harris added a commit that referenced this issue Apr 30, 2017
Rich-Harris added a commit that referenced this issue May 3, 2017
Rich-Harris added a commit that referenced this issue May 3, 2017
@Rich-Harris
Copy link
Member Author

Will close this since we have transitions now

@mizzao
Copy link

mizzao commented Apr 14, 2021

Chaining could maybe be done like this:

{{#if visible}}
  <p outro:fade='{priority: 2}'>
    this will fade out once the other <p> fade out is complete
  <p>

  <p outro:fade='{priority: 1}'>
    this will fade out first
  </p>
{{/if}}

Sorry to thread necromance 😁 but I was wondering if 4 years later there is a good practice for transition chaining. We're making a "Duolingo for cooking" PWA in Svelte (https://parsnip.ai/) and there are a lot of moments for the user where they complete something or were awarded a badge where we have to show several transitions sequentially.

For now, I've just been chaining by using manually computed delay values.

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

No branches or pull requests

5 participants