-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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 #525
Transitions #525
Conversation
Made some modest progress: Right now, just intro transitions, and only JS (timer) based ones at that. Realised that in a lot of cases you want the transition function to determine the duration of the transition (e.g. the duration of a transition might be determined by some other value e.g. position or size), so I currently prefer this API for timer-based transitions: transitions: {
myTransition ( node, params ) {
// setup code...
return {
delay: params.delay || 0,
duration: params.duration || 400,
easing: eases.cubicOut,
tick: t => {
// code fired on each animation frame
}
};
}
} CSS transitionsFor CSS-based transitions (which are generally preferable, since they don't run on the main thread, but traditionally are a lot harder to deal with), the approach I'm planning to take is to create a bespoke CSS animation for each node. That way, any JS easing function can be used — we simply create a bunch of keyframes programmatically (using the JS easing function) and progress through them with Specifying target stylestransitions: {
fly ( node, { x = 0, y = 0, delay = 0 } ) {
return {
delay: options.delay,
duration: Math.sqrt( x * x + y * y ),
easing: eases.cubicOut,
styles: {
opacity: 0,
transform: `translate(${x},${y})`
}
};
}
} In this case, we specify a map of styles to transition from (for intro transitions) or to (for outro transitions), and Svelte generates keyframes automatically: // pseudo-code
var target = getComputedStyles( node );
var keys = Object.keys( obj.styles );
for ( let i = 0; i < 1; i += step ) {
var t = ease( i );
var styles = keys
.map( key => `${key}: ${interpolate(obj.styles[ key ], target[ key ], t)}` )
.join( '; ' );
keyframes.push( `${i*100}%: { ${styles} }` );
}
var css = `@keyframes svelte_${uid} { ${keyframes.join( '\n' ) }`;
addCss( css );
node.styles.animation = `svelte_${uid} ${duration} linear`; Gets a little trickier with things like transforms because a) they're more work to interpolate, and b) you want to preserve any existing transforms. Specifying styles per-frameAlternatively, the transition function could be responsible for generating the keyframes: transitions: {
fly ( node, { x = 0, y = 0, delay = 0 } ) {
const target = getComputedStyles( node );
const opacity = +target.opacity;
return {
delay: options.delay,
duration: Math.sqrt( x * x + y * y ),
easing: eases.cubicOut,
styles: t => {
const r = 1 - t;
return `opacity: ${t * opacity}; transform: ${transform} translate(${r * x},${r * y});`
}
};
}
} Clearly that's a bit more work for the transition author (though the idea is that most of them would be installed from npm or even built-in, rather than handwritten each time), but it provides a lot of flexibility. For example, you could generate sophisticated data-driven keyframe animations such as a 12-principles-of-animation-style squash and stretch effect that took into account the size (read: 'weight') of the object that was bouncing in: import keyframes from 'svelte-transitions/keyframes.js'; // TODO...
export default {
transitions: {
fall ( node, { stretch = 0.5 } ) {
const { bottom } = node.getBoundingClientRect();
return {
duration: Math.sqrt( params.size ) * k,
// elongate and accelerate while falling, squish, bounce, then fall again
style: keyframes([
0, `transform: translate(0,${-bottom}px) scale(1,1)`,
eases.cubicIn,
0.7, `transform: translate(0,0) scale(${1 - 0.2 * stretch},${1 + 0.2 * stretch})`,
eases.cubicOut,
0.75, `transform: translate(0,0) scale(${1 + 0.5 * stretch},${1 - 0.5 * stretch})`,
eases.cubicOut
0.9, `transform: translate(0,${-bottom*0.2}px) scale(${1 - 0.1 * stretch},${1 + 0.1 * stretch})`,
eases.cubicIn,
1, `transform: translate(0,0) scale(1,1);
])
}
}
}
}; This is much more flexible and portable than approaches based on actually describing animations in CSS, but retains all of the advantages. I started writing this thinking that maybe both forms would be supported, but the more I think about it the more I think it's probably better to just have the second form. That way, Svelte doesn't need to include code for interpolating e.g. colours just in case a transition uses them. Except.... if we supported the built-in Speaking of only including necessary code: Determining whether transitions are JS-based or CSS-basedAs I've described things so far, Svelte would need to be able to accommodate both JS and CSS-based transitions, choosing which mechanism to use based on whether the transition function returns an object with a export default {
cssTransitions: {
fade: ...
},
jsTransitions: { // or rafTransitions? timerTransitions? frameTransitions?
grow: ...
}
}; Not all that user-friendly, though I suppose you could make the case that it forces component authors to know what kinds of transitions they're using and to understand the difference. Perhaps we shouldn't worry about it too much, unless it turns out to be a lot of wasted code. After all no-one should really expect transitions to be free. Other stuff I'm thinking about
All thoughts welcome. |
https://developers.google.com/web/updates/2017/03/performant-expand-and-collapse Dealt with a similar topic last month. |
@Ryuno-Ki interesting, thanks, I hadn't come across that technique. It doesn't strictly apply here as this is purely about how to manage elements that are entering or exiting the DOM, whereas the clipping technique shown assumes that the DOM elements are always there. That's the sort of thing that would best be encapsulated as a component in Svelte. Right, time for another small progress report. CSS animations now work as well as JS ones — albeit only in a subset of if-blocks for now (still need to do each-blocks and compound if-blocks). Bidirectional transitionsAfter wrestling with this problem for a while I concluded that we need to have a concept of bidirectional transitions, which look like this: (Excuse the framerate, it's a lousy GIF.) What's happening here is that (because the element has Running the transition means dynamically generating keyframes and throwing them into the DOM. We keep track of the current progress using a timer so that we can generate keyframes for a subset of the 'return journey' — I was worried this might lead to glitching but it seems to work rather well. The code for the fly transition looks like this: fly ( node, params ) {
node.style.willChange = 'transform';
const { x = 0, y = 0 } = params;
return {
delay: 0,
duration: params.duration || 400,
easing: eases.quadOut,
styles: t => {
const s = 1 - t;
return `transform: translate(${s * x}px,${s * y}px);`;
}
}
} Needless to say you could add other styles if you wanted, such as opacity. Overlapping transitionsI'm going to state an opinion: removing a block while it's introing should not cause the intro transition to abort. (I wanted to avoid opinions in favour of flexibility as far as possible, but at some point you have to have some, I think.) So if your element doesn't have a bidirectional transition, the outro will happen at the same time as the intro: Notice that the text continues to fly down while it blurs out — it doesn't snap to its end position or stay where it currently is. If you bring a block back while it's outroing, it does abort the outro. I couldn't think of a better way to handle that scenario, though if anyone has a better idea then shout. The code for that blur transition looks like this: blur ( node, params ) {
const radius = params.radius || 4;
return {
delay: params.delay || 0,
duration: params.duration,
styles: t => {
return `opacity: ${t}; filter: blur(${radius * ( 1 - t )}px);`;
}
};
} Easing equationsRight now, for the easing on the fly transition we're importing eases-jsnext. That's fine, but it means that if someone wanted to specify one of those easing functions in their parameters, it would look like this: <div in:someTransition='{easing:"elasticOut"}'>...</div>
<script>
import * as eases from 'eases-jsnext';
export default {
transitions: {
someTransition ( node, params ) {
return {
easing: eases[ params.easing ],
// ...
};
}
}
};
</script> The dynamic namespace lookup defeats tree-shaking. So I'm wondering if maybe Svelte could expose those easing functions, and replace Is that too magical? |
… removed), and trim trailing tabs
Ok, I've taken the WIP tag off the title, if anyone is masochistic enough to review this PR... Transitions should still be considered experimental for the moment — there are some known bugs around keyed |
@@ -168,7 +169,7 @@ export default function dom ( parsed, source, options ) { | |||
if ( templateProperties.oncreate ) { | |||
builders.init.addBlock( deindent` | |||
if ( options._root ) { | |||
options._root._renderHooks.push({ fn: ${generator.alias( 'template' )}.oncreate, context: this }); | |||
options._root._renderHooks.push( ${generator.alias( 'template' )}.oncreate.bind( this ) ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since .bind
is slower than pretty much every other option of calling a function later, we could do something like
var self = this;
options._root._renderHooks.push( function () {
${ generator.alias( ' template' ) }.oncreate.call ( self );
} );
which would be slightly faster. Here's a test showing this: https://jsperf.com/bind-vs-self-closure (interestingly enough the calling of a .bind
ed function is no slower than the closure in firefox, but in chrome it's 10x slower)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's probably better, yeah. If we wanted to be really clever, the fastest way to run oncreate
would probably be to rewrite references to this
, the same way we do in non-hoisted event handlers:
// this...
oncreate () {
this.observe( 'foo', function ( foo ) {
this.refs.whatever.value = foo.toUpperCase();
});
}
// ...becomes this:
oncreate ( component ) {
component.observe( 'foo', function ( foo ) {
this.refs.whatever.value = foo.toUpperCase();
});
}
Or is that a terrible idea?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(obviously that only works if oncreate
is declared inline — if it's a reference we'd still need to .call
it)
next: function () { | ||
transitionManager.running = false; | ||
|
||
var now = window.performance.now(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to support older versions of IE? Not sure if there's been a ton of discussion on it, but in this case performance.now()
is only supported in IE10+ https://developer.mozilla.org/en-US/docs/Web/API/Performance/now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, good question. I don't have a strong view. I reckon it's probably ok, since it's easily polyfilled, but perhaps we need to think about having a section of the docs that says which polyfills you'll likely need for which version of IE.
I think I got through a third of it, but understanding the logic of it in the browser is tough. I'll pull this out in an IDE soon and take another peek. |
… style/styles confusion
Am basically happy with the state this is in — renamed Last task before merging is to follow up #553 — unfortunately the trick of stringifying functions and comparing the export name with the function name doesn't work for |
|
Codecov Report
@@ Coverage Diff @@
## master #525 +/- ##
========================================
Coverage ? 88.2%
========================================
Files ? 95
Lines ? 2933
Branches ? 0
========================================
Hits ? 2587
Misses ? 346
Partials ? 0
Continue to review full report at Codecov.
|
https://travis-ci.org/sveltejs/svelte/jobs/228412243 No destructuring in Node 4 👎 Please. Node 4. Please. |
Not sure what's going on with codecov, will ignore those errors for now |
I really don't understand why transitions or easing functions should be part of Svelte. IMO, the best libraries and frameworks are typically that do as little as possible but do it better than every competitor. IMO transitions are way beyond the scope of a project like Svelte and adding too much of these features in core would actually be a reason for me to avoid using Svelte. I also don't think that transitions are something that belong in a plugin. Instead, there are the kind of features I'd expect to find in a framework independent utility library that I can plug into React, Vue, or Svelte or whichever framework I'm using. I'm also not convinced that JS is the right place for parameterizable easing functions that generate CSS. This is precisely the kind of use cases CSS preprocessors are created for. If transitions with custom easing is a feature important to you, why not focus on adding Less or Sass support to Svelte first (#181) and then import transitions & easing from a separate Less or Sass library that can also be plugged into React, Vue & other JS frameworks? See also #549 (comment). |
I'm probably the person on the team who is most against adding features to svelte that don't need to be there, but I disagree. Things like transitions fit well into Svelte because they're things that heavily depend on the state of a component, which gives Svelte the unique advantage of being able to apply them only when needed. Adding new features to React or Vue causes performance and size implications because you're requiring their entire bundle. You can see React getting around this slightly by previously using require('react/lib/xx') and in the future having multiple modules to include additional features. |
Sure... But as you said elsewhere, this does tend to attract attention away from the core library and adds additional maintenance for a team that probably already has way too little time. This, in turn, prevents the core library from reaching the level of maturity it needs to compete with React or Vue... which itself is needed to attract more users & maintainers alike... Creating and maintaining plugins alongside the core library is all fine and dandy, but if it distracts away too much attention from the core library it can end up becoming pretty problematic. |
This will probably be WIP for a while, as there will no doubt be lots of 'fun' challenges to solve along the way. So far, just adding support for
in
,out
andtransition
directives (wheretransition
just means 'bothin
andout
').