1
- /** @import { AnimateFn, Animation, AnimationConfig, EachItem, Effect, Task, TransitionFn, TransitionManager } from '#client' */
1
+ /** @import { AnimateFn, Animation, AnimationConfig, EachItem, Effect, TransitionFn, TransitionManager } from '#client' */
2
2
import { noop , is_function } from '../../../shared/utils.js' ;
3
3
import { effect } from '../../reactivity/effects.js' ;
4
4
import { current_effect , untrack } from '../../runtime.js' ;
5
- import { raf } from '../../timing.js' ;
6
5
import { loop } from '../../loop.js' ;
7
6
import { should_intro } from '../../render.js' ;
8
7
import { current_each_item } from '../blocks/each.js' ;
@@ -97,17 +96,10 @@ export function animation(element, get_fn, get_params) {
97
96
) {
98
97
const options = get_fn ( ) ( this . element , { from, to } , get_params ?. ( ) ) ;
99
98
100
- animation = animate (
101
- this . element ,
102
- options ,
103
- undefined ,
104
- 1 ,
105
- ( ) => {
106
- animation ?. abort ( ) ;
107
- animation = undefined ;
108
- } ,
109
- undefined
110
- ) ;
99
+ animation = animate ( this . element , options , undefined , 1 , ( ) => {
100
+ animation ?. abort ( ) ;
101
+ animation = undefined ;
102
+ } ) ;
111
103
}
112
104
} ,
113
105
fix ( ) {
@@ -192,14 +184,13 @@ export function transition(flags, element, get_fn, get_params) {
192
184
/** @type {Animation | undefined } */
193
185
var outro ;
194
186
195
- /** @type {(() => void) | undefined } */
196
- var reset ;
197
-
198
187
function get_options ( ) {
199
188
// If a transition is still ongoing, we use the existing options rather than generating
200
189
// new ones. This ensures that reversible transitions reverse smoothly, rather than
201
190
// jumping to a new spot because (for example) a different `duration` was used
202
- return ( current_options ??= get_fn ( ) ( element , get_params ?. ( ) , { direction } ) ) ;
191
+ return ( current_options ??= get_fn ( ) ( element , get_params ?. ( ) ?? /** @type {P } */ ( { } ) , {
192
+ direction
193
+ } ) ) ;
203
194
}
204
195
205
196
/** @type {TransitionManager } */
@@ -208,65 +199,43 @@ export function transition(flags, element, get_fn, get_params) {
208
199
in ( ) {
209
200
element . inert = inert ;
210
201
211
- // abort the outro to prevent overlap with the intro
212
- outro ?. abort ( ) ;
213
- // abort previous intro (can happen if an element is intro'd, then outro'd, then intro'd again)
214
- intro ?. abort ( ) ;
202
+ if ( ! is_intro ) {
203
+ outro ?. abort ( ) ;
204
+ outro ?. reset ?. ( ) ;
205
+ return ;
206
+ }
215
207
216
- if ( is_intro ) {
217
- dispatch_event ( element , 'introstart' ) ;
218
- intro = animate (
219
- element ,
220
- get_options ( ) ,
221
- outro ,
222
- 1 ,
223
- ( ) => {
224
- dispatch_event ( element , 'introend' ) ;
225
- // Ensure we cancel the animation to prevent leaking
226
- intro ?. abort ( ) ;
227
- intro = current_options = undefined ;
228
- } ,
229
- is_both
230
- ? undefined
231
- : ( ) => {
232
- intro = current_options = undefined ;
233
- }
234
- ) ;
235
- } else {
236
- reset ?. ( ) ;
208
+ if ( ! is_outro ) {
209
+ // if we intro then outro then intro again, we want to abort the first intro,
210
+ // if it's not a bidirectional transition
211
+ intro ?. abort ( ) ;
237
212
}
213
+
214
+ dispatch_event ( element , 'introstart' ) ;
215
+
216
+ intro = animate ( element , get_options ( ) , outro , 1 , ( ) => {
217
+ dispatch_event ( element , 'introend' ) ;
218
+
219
+ // Ensure we cancel the animation to prevent leaking
220
+ intro ?. abort ( ) ;
221
+ intro = current_options = undefined ;
222
+ } ) ;
238
223
} ,
239
224
out ( fn ) {
240
- // abort previous outro (can happen if an element is outro'd, then intro'd, then outro'd again)
241
- outro ?. abort ( ) ;
242
-
243
- if ( is_outro ) {
244
- element . inert = true ;
245
-
246
- dispatch_event ( element , 'outrostart' ) ;
247
- outro = animate (
248
- element ,
249
- get_options ( ) ,
250
- intro ,
251
- 0 ,
252
- ( ) => {
253
- dispatch_event ( element , 'outroend' ) ;
254
- outro = current_options = undefined ;
255
- fn ?. ( ) ;
256
- } ,
257
- is_both
258
- ? undefined
259
- : ( ) => {
260
- outro = current_options = undefined ;
261
- }
262
- ) ;
263
-
264
- // TODO arguably the outro should never null itself out until _all_ outros for this effect have completed...
265
- // in that case we wouldn't need to store `reset` separately
266
- reset = outro . reset ;
267
- } else {
225
+ if ( ! is_outro ) {
268
226
fn ?. ( ) ;
227
+ current_options = undefined ;
228
+ return ;
269
229
}
230
+
231
+ element . inert = true ;
232
+
233
+ dispatch_event ( element , 'outrostart' ) ;
234
+
235
+ outro = animate ( element , get_options ( ) , intro , 0 , ( ) => {
236
+ dispatch_event ( element , 'outroend' ) ;
237
+ fn ?. ( ) ;
238
+ } ) ;
270
239
} ,
271
240
stop : ( ) => {
272
241
intro ?. abort ( ) ;
@@ -282,7 +251,7 @@ export function transition(flags, element, get_fn, get_params) {
282
251
// parent (block) effect is where the state change happened. we can determine that by
283
252
// looking at whether the block effect is currently initializing
284
253
if ( is_intro && should_intro ) {
285
- let run = is_global ;
254
+ var run = is_global ;
286
255
287
256
if ( ! run ) {
288
257
var block = /** @type {Effect | null } */ ( e . parent ) ;
@@ -311,25 +280,24 @@ export function transition(flags, element, get_fn, get_params) {
311
280
* @param {AnimationConfig | ((opts: { direction: 'in' | 'out' }) => AnimationConfig) } options
312
281
* @param {Animation | undefined } counterpart The corresponding intro/outro to this outro/intro
313
282
* @param {number } t2 The target `t` value — `1` for intro, `0` for outro
314
- * @param {(() => void) | undefined } on_finish Called after successfully completing the animation
315
- * @param {(() => void) | undefined } on_abort Called if the animation is aborted
283
+ * @param {(() => void) } on_finish Called after successfully completing the animation
316
284
* @returns {Animation }
317
285
*/
318
- function animate ( element , options , counterpart , t2 , on_finish , on_abort ) {
286
+ function animate ( element , options , counterpart , t2 , on_finish ) {
319
287
var is_intro = t2 === 1 ;
320
288
321
289
if ( is_function ( options ) ) {
322
290
// In the case of a deferred transition (such as `crossfade`), `option` will be
323
291
// a function rather than an `AnimationConfig`. We need to call this function
324
- // once DOM has been updated...
292
+ // once the DOM has been updated...
325
293
/** @type {Animation } */
326
294
var a ;
327
295
var aborted = false ;
328
296
329
297
queue_micro_task ( ( ) => {
330
298
if ( aborted ) return ;
331
299
var o = options ( { direction : is_intro ? 'in' : 'out' } ) ;
332
- a = animate ( element , o , counterpart , t2 , on_finish , on_abort ) ;
300
+ a = animate ( element , o , counterpart , t2 , on_finish ) ;
333
301
} ) ;
334
302
335
303
// ...but we want to do so without using `async`/`await` everywhere, so
@@ -341,14 +309,15 @@ function animate(element, options, counterpart, t2, on_finish, on_abort) {
341
309
} ,
342
310
deactivate : ( ) => a . deactivate ( ) ,
343
311
reset : ( ) => a . reset ( ) ,
344
- t : ( now ) => a . t ( now )
312
+ t : ( ) => a . t ( )
345
313
} ;
346
314
}
347
315
348
316
counterpart ?. deactivate ( ) ;
349
317
350
318
if ( ! options ?. duration ) {
351
- on_finish ?. ( ) ;
319
+ on_finish ( ) ;
320
+
352
321
return {
353
322
abort : noop ,
354
323
deactivate : noop ,
@@ -359,90 +328,73 @@ function animate(element, options, counterpart, t2, on_finish, on_abort) {
359
328
360
329
const { delay = 0 , css, tick, easing = linear } = options ;
361
330
362
- var start = raf . now ( ) + delay ;
363
- var t1 = counterpart ?. t ( start ) ?? 1 - t2 ;
364
- var delta = t2 - t1 ;
331
+ var keyframes = [ ] ;
365
332
366
- var duration = options . duration * Math . abs ( delta ) ;
367
- var end = start + duration ;
333
+ if ( is_intro && counterpart === undefined ) {
334
+ if ( tick ) {
335
+ tick ( 0 , 1 ) ; // TODO put in nested effect, to avoid interleaved reads/writes?
336
+ }
368
337
369
- /** @type {globalThis.Animation } */
370
- var animation ;
338
+ if ( css ) {
339
+ var styles = css_to_keyframe ( css ( 0 , 1 ) ) ;
340
+ keyframes . push ( styles , styles ) ;
341
+ }
342
+ }
371
343
372
- /** @type {Task } */
373
- var task ;
344
+ var get_t = ( ) => 1 - t2 ;
374
345
375
- if ( css ) {
376
- // run after a micro task so that all transitions that are lining up and are about to run can correctly measure the DOM
377
- queue_micro_task ( ( ) => {
378
- // WAAPI
379
- var keyframes = [ ] ;
380
- var n = Math . ceil ( duration / ( 1000 / 60 ) ) ; // `n` must be an integer, or we risk missing the `t2` value
346
+ // create a dummy animation that lasts as long as the delay (but with whatever devtools
347
+ // multiplier is in effect). in the common case that it is `0`, we keep it anyway so that
348
+ // the CSS keyframes aren't created until the DOM is updated
349
+ var animation = element . animate ( keyframes , { duration : delay } ) ;
381
350
382
- // In case of a delayed intro, apply the initial style for the duration of the delay;
383
- // else in case of a fade-in for example the element would be visible until the animation starts
384
- if ( is_intro && delay > 0 ) {
385
- let m = Math . ceil ( delay / ( 1000 / 60 ) ) ;
386
- let keyframe = css_to_keyframe ( css ( 0 , 1 ) ) ;
387
- for ( let i = 0 ; i < m ; i += 1 ) {
388
- keyframes . push ( keyframe ) ;
389
- }
390
- }
351
+ animation . onfinish = ( ) => {
352
+ // for bidirectional transitions, we start from the current position,
353
+ // rather than doing a full intro/outro
354
+ var t1 = counterpart ?. t ( ) ?? 1 - t2 ;
355
+ counterpart ?. abort ( ) ;
356
+
357
+ var delta = t2 - t1 ;
358
+ var duration = /** @type {number } */ ( options . duration ) * Math . abs ( delta ) ;
359
+ var keyframes = [ ] ;
360
+
361
+ if ( css ) {
362
+ var n = Math . ceil ( duration / ( 1000 / 60 ) ) ; // `n` must be an integer, or we risk missing the `t2` value
391
363
392
364
for ( var i = 0 ; i <= n ; i += 1 ) {
393
365
var t = t1 + delta * easing ( i / n ) ;
394
366
var styles = css ( t , 1 - t ) ;
395
367
keyframes . push ( css_to_keyframe ( styles ) ) ;
396
368
}
369
+ }
397
370
398
- animation = element . animate ( keyframes , {
399
- delay : is_intro ? 0 : delay ,
400
- duration : duration + ( is_intro ? delay : 0 ) ,
401
- easing : 'linear' ,
402
- fill : 'forwards'
403
- } ) ;
371
+ animation = element . animate ( keyframes , { duration, fill : 'forwards' } ) ;
404
372
405
- animation . finished
406
- . then ( ( ) => {
407
- on_finish ?. ( ) ;
408
-
409
- if ( t2 === 1 ) {
410
- animation . cancel ( ) ;
411
- }
412
- } )
413
- . catch ( ( e ) => {
414
- // Error for DOMException: The user aborted a request. This results in two things:
415
- // - startTime is `null`
416
- // - currentTime is `null`
417
- // We can't use the existence of an AbortError as this error and error code is shared
418
- // with other Web APIs such as fetch().
419
-
420
- if ( animation . startTime !== null && animation . currentTime !== null ) {
421
- throw e ;
422
- }
423
- } ) ;
424
- } ) ;
425
- } else {
426
- // Timer
427
- if ( t1 === 0 ) {
428
- tick ?. ( 0 , 1 ) ; // TODO put in nested effect, to avoid interleaved reads/writes?
429
- }
373
+ animation . onfinish = ( ) => {
374
+ get_t = ( ) => t2 ;
375
+ tick ?. ( t2 , 1 - t2 ) ;
376
+ on_finish ( ) ;
377
+ } ;
430
378
431
- task = loop ( ( now ) => {
432
- if ( now >= end ) {
433
- tick ?. ( t2 , 1 - t2 ) ;
434
- on_finish ?. ( ) ;
435
- return false ;
436
- }
379
+ get_t = ( ) => {
380
+ var time = /** @type {number } */ (
381
+ /** @type {globalThis.Animation } */ ( animation ) . currentTime
382
+ ) ;
437
383
438
- if ( now >= start ) {
439
- var p = t1 + delta * easing ( ( now - start ) / duration ) ;
440
- tick ?. ( p , 1 - p ) ;
441
- }
384
+ return t1 + delta * easing ( time / duration ) ;
385
+ } ;
442
386
443
- return true ;
444
- } ) ;
445
- }
387
+ if ( tick ) {
388
+ loop ( ( ) => {
389
+ if ( animation . playState !== 'running' ) return false ;
390
+
391
+ var t = get_t ( ) ;
392
+ tick ( t , 1 - t ) ;
393
+
394
+ return true ;
395
+ } ) ;
396
+ }
397
+ } ;
446
398
447
399
return {
448
400
abort : ( ) => {
@@ -451,23 +403,15 @@ function animate(element, options, counterpart, t2, on_finish, on_abort) {
451
403
// This prevents memory leaks in Chromium
452
404
animation . effect = null ;
453
405
}
454
- task ?. abort ( ) ;
455
- on_abort ?. ( ) ;
456
- on_finish = undefined ;
457
- on_abort = undefined ;
458
406
} ,
459
407
deactivate : ( ) => {
460
- on_finish = undefined ;
461
- on_abort = undefined ;
408
+ on_finish = noop ;
462
409
} ,
463
410
reset : ( ) => {
464
411
if ( t2 === 0 ) {
465
412
tick ?. ( 1 , 0 ) ;
466
413
}
467
414
} ,
468
- t : ( now ) => {
469
- var t = t1 + delta * easing ( ( now - start ) / duration ) ;
470
- return Math . min ( 1 , Math . max ( 0 , t ) ) ;
471
- }
415
+ t : ( ) => get_t ( )
472
416
} ;
473
417
}
0 commit comments