@@ -482,3 +482,290 @@ describe("re-rendering leaves the rest as the seek left it", () => {
482482 expect ( timeline . duration ( ) ) . toBe ( 20 ) ;
483483 } ) ;
484484} ) ;
485+
486+ describe ( "gsap frame refresh from-state" , ( ) => {
487+ it ( "keeps a from-state when the tween starts between output frames" , ( ) => {
488+ const target = { x : 0 , pivot : 50 } ;
489+ const timeline = gsap
490+ . timeline ( { paused : true } )
491+ . fromTo (
492+ target ,
493+ { x : 10 , pivot : 0 } ,
494+ { x : 100 , duration : 1 , ease : "none" , immediateRender : false } ,
495+ 0.0995 ,
496+ ) ;
497+ const adapter = createGsapAdapter ( {
498+ getTimeline : ( ) => timeline as unknown as RuntimeTimelineLike ,
499+ } ) ;
500+ adapter . seek ( { time : 3 / 30 } ) ;
501+ expect ( target . pivot ) . toBe ( 0 ) ;
502+ expect ( target . x ) . toBeCloseTo ( 10.045 , 6 ) ;
503+ timeline . kill ( ) ;
504+ } ) ;
505+
506+ it ( "keeps from-only values after a short tween ends within the redraw's rewind" , ( ) => {
507+ const target = { x : 0 , pivot : 50 } ;
508+ const timeline = gsap
509+ . timeline ( { paused : true } )
510+ . fromTo (
511+ target ,
512+ { x : 10 , pivot : 0 } ,
513+ { x : 100 , duration : 0.0002 , ease : "none" , immediateRender : false } ,
514+ 0.1 ,
515+ ) ;
516+ timeline . set ( { } , { } , 1 ) ;
517+ const adapter = createGsapAdapter ( {
518+ getTimeline : ( ) => timeline as unknown as RuntimeTimelineLike ,
519+ } ) ;
520+ adapter . seek ( { time : 0.1005 } ) ;
521+ expect ( target . pivot ) . toBe ( 0 ) ;
522+ expect ( target . x ) . toBe ( 100 ) ;
523+ timeline . kill ( ) ;
524+ } ) ;
525+
526+ it ( "restores the normal tween render when replaying a from-state throws" , ( ) => {
527+ let pivot = 50 ;
528+ let rejectFromState = false ;
529+ const target = {
530+ x : 0 ,
531+ get pivot ( ) {
532+ return pivot ;
533+ } ,
534+ set pivot ( value : number ) {
535+ if ( rejectFromState && value === 0 ) throw new Error ( "from-state rejected" ) ;
536+ pivot = value ;
537+ } ,
538+ } ;
539+ const timeline = gsap
540+ . timeline ( { paused : true } )
541+ . fromTo (
542+ target ,
543+ { x : 10 , pivot : 0 } ,
544+ { x : 100 , duration : 1 , ease : "none" , immediateRender : false } ,
545+ 0.1 ,
546+ ) ;
547+ timeline . totalTime ( 0.1 , true ) ;
548+ const tween = timeline . getChildren ( false , true , false ) [ 0 ] ! ;
549+ const renderDescriptor = Object . getOwnPropertyDescriptor ( tween , "render" ) ;
550+ rejectFromState = true ;
551+ const adapter = createGsapAdapter ( {
552+ getTimeline : ( ) => timeline as unknown as RuntimeTimelineLike ,
553+ } ) ;
554+ expect ( ( ) => adapter . seek ( { time : 0.1 } ) ) . toThrow ( "from-state rejected" ) ;
555+ expect ( Object . getOwnPropertyDescriptor ( tween , "render" ) ) . toEqual ( renderDescriptor ) ;
556+ expect ( ( ) => adapter . seek ( { time : 0.2 } ) ) . not . toThrow ( ) ;
557+ expect ( target . x ) . toBeCloseTo ( 19 , 6 ) ;
558+ timeline . kill ( ) ;
559+ } ) ;
560+
561+ it . each ( [ false , true ] ) ( "restores the tween's render descriptor (own=%s)" , ( own ) => {
562+ const target = { x : 0 , pivot : 50 } ;
563+ const timeline = gsap . timeline ( { paused : true } ) ;
564+ timeline . fromTo (
565+ target ,
566+ { x : 10 , pivot : 0 } ,
567+ { x : 100 , duration : 1 , ease : "none" , immediateRender : false } ,
568+ 0.0995 ,
569+ ) ;
570+ const tween = timeline . getChildren ( false , true , false ) [ 0 ] ! ;
571+ if ( own ) {
572+ Object . defineProperty ( tween , "render" , {
573+ value : tween . render ,
574+ writable : true ,
575+ configurable : true ,
576+ enumerable : false ,
577+ } ) ;
578+ }
579+ const renderDescriptor = Object . getOwnPropertyDescriptor ( tween , "render" ) ;
580+ const adapter = createGsapAdapter ( {
581+ getTimeline : ( ) => timeline as unknown as RuntimeTimelineLike ,
582+ } ) ;
583+ adapter . seek ( { time : 0.1 } ) ;
584+ expect ( target . x ) . toBeCloseTo ( 10.045 , 6 ) ;
585+ expect ( Object . getOwnPropertyDescriptor ( tween , "render" ) ) . toEqual ( renderDescriptor ) ;
586+ timeline . kill ( ) ;
587+ } ) ;
588+
589+ it ( "keeps interpolation when a from-state setter reenters the refresh" , ( ) => {
590+ let pivot = 50 ;
591+ let armed = false ;
592+ let entered = false ;
593+ const target = {
594+ x : 0 ,
595+ get pivot ( ) {
596+ return pivot ;
597+ } ,
598+ set pivot ( value : number ) {
599+ pivot = value ;
600+ if ( armed && value === 0 && ! entered ) {
601+ entered = true ;
602+ rerenderGsapTimelineAt ( timeline , 0.1005 ) ;
603+ }
604+ } ,
605+ } ;
606+ const timeline = gsap
607+ . timeline ( { paused : true } )
608+ . fromTo (
609+ target ,
610+ { x : 10 , pivot : 0 } ,
611+ { x : 100 , duration : 1 , ease : "none" , immediateRender : false } ,
612+ 0.1 ,
613+ ) ;
614+ timeline . totalTime ( 0.1005 , true ) ;
615+ armed = true ;
616+ rerenderGsapTimelineAt ( timeline , 0.1005 ) ;
617+ expect ( entered ) . toBe ( true ) ;
618+ expect ( target . pivot ) . toBe ( 0 ) ;
619+ expect ( target . x ) . toBeCloseTo ( 10.045 , 6 ) ;
620+ timeline . kill ( ) ;
621+ } ) ;
622+
623+ it ( "preserves repeatRefresh function evaluation for very short immediate tweens" , ( ) => {
624+ let calls = 0 ;
625+ const target = { x : 0 , pivot : 50 } ;
626+ const timeline = gsap
627+ . timeline ( { paused : true } )
628+ . fromTo (
629+ target ,
630+ { x : ( ) => 10 * ++ calls , pivot : 0 } ,
631+ {
632+ x : 100 ,
633+ duration : 0.0002 ,
634+ repeat : 2 ,
635+ repeatRefresh : true ,
636+ immediateRender : true ,
637+ ease : "none" ,
638+ } ,
639+ 0.1 ,
640+ )
641+ . to ( { } , { duration : 1 } ) ;
642+ const adapter = createGsapAdapter ( {
643+ getTimeline : ( ) => timeline as unknown as RuntimeTimelineLike ,
644+ } ) ;
645+ for ( const time of [ 0 , 0.1 , 0.1005 ] ) adapter . seek ( { time } ) ;
646+ expect ( target . x ) . toBe ( 80 ) ;
647+ expect ( calls ) . toBe ( 6 ) ;
648+ timeline . kill ( ) ;
649+ } ) ;
650+
651+ it ( "reuses the initialized function-valued from-state on repeated seeks" , ( ) => {
652+ let calls = 0 ;
653+ const target = { x : 0 , pivot : 50 } ;
654+ const timeline = gsap . timeline ( { paused : true } ) . fromTo (
655+ target ,
656+ {
657+ x : 10 ,
658+ pivot : ( ) => {
659+ calls ++ ;
660+ return 0 ;
661+ } ,
662+ } ,
663+ { x : 100 , duration : 1 , ease : "none" , immediateRender : false } ,
664+ 0.1 ,
665+ ) ;
666+ const adapter = createGsapAdapter ( {
667+ getTimeline : ( ) => timeline as unknown as RuntimeTimelineLike ,
668+ } ) ;
669+ for ( const time of [ 0.1 , 0.1 , 0.1005 ] ) adapter . seek ( { time } ) ;
670+ expect ( target . pivot ) . toBe ( 0 ) ;
671+ expect ( calls ) . toBe ( 1 ) ;
672+ timeline . kill ( ) ;
673+ } ) ;
674+
675+ it . each ( [
676+ { label : "exactly at the start" , at : 0.1 , x : 10 } ,
677+ { label : "within the redraw's rewind" , at : 0.1005 , x : 10.045 } ,
678+ { label : "after the redraw's rewind" , at : 0.2 , x : 19 } ,
679+ ] ) ( "keeps from-only values $label and restores them before the tween" , ( { at, x } ) => {
680+ const target = { x : 0 , pivot : 50 } ;
681+ const timeline = gsap
682+ . timeline ( { paused : true } )
683+ . fromTo (
684+ target ,
685+ { x : 10 , pivot : 0 } ,
686+ { x : 100 , duration : 1 , ease : "none" , immediateRender : false , autoRevert : true } ,
687+ 0.1 ,
688+ ) ;
689+ const adapter = createGsapAdapter ( {
690+ getTimeline : ( ) => timeline as unknown as RuntimeTimelineLike ,
691+ } ) ;
692+ adapter . seek ( { time : at } ) ;
693+ adapter . seek ( { time : at } ) ;
694+ expect ( target . pivot ) . toBe ( 0 ) ;
695+ expect ( target . x ) . toBeCloseTo ( x , 6 ) ;
696+ adapter . seek ( { time : 0.05 } ) ;
697+ expect ( target . pivot ) . toBe ( 50 ) ;
698+ expect ( target . x ) . toBe ( 0 ) ;
699+ timeline . kill ( ) ;
700+ } ) ;
701+
702+ it . each ( [ 1 , 2 ] ) ( "keeps from-only values in a scene playing at %sx" , ( speed ) => {
703+ const target = { x : 0 , pivot : 50 } ;
704+ const scene = gsap
705+ . timeline ( )
706+ . fromTo (
707+ target ,
708+ { x : 10 , pivot : 0 } ,
709+ { x : 100 , duration : 1 , ease : "none" , immediateRender : false } ,
710+ 0.1 ,
711+ )
712+ . timeScale ( speed ) ;
713+ const timeline = gsap . timeline ( { paused : true } ) . add ( scene , 1 ) ;
714+ const adapter = createGsapAdapter ( {
715+ getTimeline : ( ) => timeline as unknown as RuntimeTimelineLike ,
716+ } ) ;
717+ adapter . seek ( { time : 1 + 0.1 / speed } ) ;
718+ expect ( target . pivot ) . toBe ( 0 ) ;
719+ expect ( target . x ) . toBeCloseTo ( 10 , 6 ) ;
720+ timeline . kill ( ) ;
721+ } ) ;
722+
723+ it . each ( [ false , true ] ) ( "keeps from-only writers in authored order (set last: %s)" , ( setLast ) => {
724+ const target = { x : 0 , pivot : 50 } ;
725+ const timeline = gsap . timeline ( { paused : true } ) ;
726+ const set = ( ) => timeline . set ( target , { pivot : 42 } , 0.1 ) ;
727+ const fromTo = ( ) =>
728+ timeline . fromTo (
729+ target ,
730+ { x : 10 , pivot : 0 } ,
731+ { x : 100 , duration : 1 , ease : "none" , immediateRender : false } ,
732+ 0.1 ,
733+ ) ;
734+ if ( setLast ) {
735+ fromTo ( ) ;
736+ set ( ) ;
737+ } else {
738+ set ( ) ;
739+ fromTo ( ) ;
740+ }
741+ const adapter = createGsapAdapter ( {
742+ getTimeline : ( ) => timeline as unknown as RuntimeTimelineLike ,
743+ } ) ;
744+ for ( const time of [ 0.1 , 0.1 , 0.2 ] ) {
745+ adapter . seek ( { time } ) ;
746+ expect ( target . pivot ) . toBe ( setLast ? 42 : 0 ) ;
747+ }
748+ timeline . kill ( ) ;
749+ } ) ;
750+
751+ it ( "leaves uninitialized scene siblings alone at the scene's opening" , ( ) => {
752+ const target = { x : 0 } ;
753+ const other = { x : 0 , pivot : 50 } ;
754+ const timeline = gsap
755+ . timeline ( { paused : true } )
756+ . to ( target , { x : 100 , duration : 10 , ease : "none" } ) ;
757+ const scene = gsap
758+ . timeline ( )
759+ . to ( target , { x : 200 , duration : 1 , overwrite : "auto" , ease : "none" } , 0 ) ;
760+ scene . fromTo ( other , { x : 10 , pivot : 0 } , { x : 100 , duration : 1 , immediateRender : false } , 0 ) ;
761+ timeline . add ( scene , 2 ) ;
762+ const adapter = createGsapAdapter ( {
763+ getTimeline : ( ) => timeline as unknown as RuntimeTimelineLike ,
764+ } ) ;
765+ adapter . seek ( { time : 2 } ) ;
766+ adapter . seek ( { time : 1 } ) ;
767+ expect ( target . x ) . toBe ( 10 ) ;
768+ expect ( other ) . toMatchObject ( { x : 0 , pivot : 50 } ) ;
769+ timeline . kill ( ) ;
770+ } ) ;
771+ } ) ;
0 commit comments