@@ -735,3 +735,152 @@ test('Animated.sequence', () => {
735735
736736 expect ( _isSequenceFinished ) . toBe ( true ) ;
737737} ) ;
738+
739+ describe ( 'Animated.spring' , ( ) => {
740+ test ( 'basic spring animation' , ( ) => {
741+ let _translateX ;
742+ const viewRef = createRef < HostInstance > ( ) ;
743+
744+ function MyApp ( ) {
745+ const translateX = useAnimatedValue ( 0 ) ;
746+ _translateX = translateX ;
747+ return (
748+ < Animated . View
749+ ref = { viewRef }
750+ style = { [ { width : 100 , height : 100 } , { transform : [ { translateX} ] } ] }
751+ />
752+ ) ;
753+ }
754+
755+ const root = Fantom . createRoot ( ) ;
756+ Fantom . runTask ( ( ) => {
757+ root . render ( < MyApp /> ) ;
758+ } ) ;
759+
760+ const viewElement = ensureInstance ( viewRef . current , ReactNativeElement ) ;
761+ expect ( viewElement . getBoundingClientRect ( ) . x ) . toBe ( 0 ) ;
762+
763+ let finishResult = null ;
764+ Fantom . runTask ( ( ) => {
765+ Animated . spring ( _translateX , {
766+ toValue : 100 ,
767+ stiffness : 100 ,
768+ damping : 10 ,
769+ mass : 1 ,
770+ useNativeDriver : true ,
771+ } ) . start ( result => {
772+ finishResult = result ;
773+ } ) ;
774+ } ) ;
775+
776+ Fantom . unstable_produceFramesForDuration ( 500 ) ;
777+
778+ const transform =
779+ // $FlowFixMe[incompatible-use]
780+ Fantom . unstable_getDirectManipulationProps ( viewElement ) . transform [ 0 ] ;
781+ expect ( transform . translateX ) . toBeGreaterThan ( 0 ) ;
782+
783+ Fantom . unstable_produceFramesForDuration ( 4500 ) ;
784+
785+ Fantom . runWorkLoop ( ) ;
786+ expect ( viewElement . getBoundingClientRect ( ) . x ) . toBe ( 100 ) ;
787+ expect ( finishResult ?. finished ) . toBe ( true ) ;
788+ } ) ;
789+
790+ test ( 'with overshoot clamping' , ( ) => {
791+ let _translateX ;
792+ const viewRef = createRef < HostInstance > ( ) ;
793+
794+ function MyApp ( ) {
795+ const translateX = useAnimatedValue ( 0 ) ;
796+ _translateX = translateX ;
797+ return (
798+ < Animated . View
799+ ref = { viewRef }
800+ style = { [ { width : 100 , height : 100 } , { transform : [ { translateX} ] } ] }
801+ />
802+ ) ;
803+ }
804+
805+ const root = Fantom . createRoot ( ) ;
806+ Fantom . runTask ( ( ) => {
807+ root . render ( < MyApp /> ) ;
808+ } ) ;
809+
810+ const viewElement = ensureInstance ( viewRef . current , ReactNativeElement ) ;
811+
812+ Fantom . runTask ( ( ) => {
813+ Animated . spring ( _translateX , {
814+ toValue : 100 ,
815+ stiffness : 300 ,
816+ damping : 5 ,
817+ mass : 1 ,
818+ overshootClamping : true ,
819+ useNativeDriver : true ,
820+ } ) . start ( ) ;
821+ } ) ;
822+
823+ // Sample at multiple points to ensure value never exceeds toValue
824+ for ( let i = 0 ; i < 25 ; i ++ ) {
825+ Fantom . unstable_produceFramesForDuration ( 200 ) ;
826+ const t =
827+ // $FlowFixMe[incompatible-use]
828+ Fantom . unstable_getDirectManipulationProps ( viewElement ) . transform [ 0 ] ;
829+ expect ( t . translateX ) . toBeLessThanOrEqual ( 100 ) ;
830+ expect ( t . translateX ) . toBeGreaterThanOrEqual ( 0 ) ;
831+ }
832+
833+ Fantom . runWorkLoop ( ) ;
834+ expect ( viewElement . getBoundingClientRect ( ) . x ) . toBe ( 100 ) ;
835+ } ) ;
836+
837+ test ( 'critically damped' , ( ) => {
838+ let _translateX ;
839+ const viewRef = createRef < HostInstance > ( ) ;
840+
841+ function MyApp ( ) {
842+ const translateX = useAnimatedValue ( 0 ) ;
843+ _translateX = translateX ;
844+ return (
845+ < Animated . View
846+ ref = { viewRef }
847+ style = { [ { width : 100 , height : 100 } , { transform : [ { translateX} ] } ] }
848+ />
849+ ) ;
850+ }
851+
852+ const root = Fantom . createRoot ( ) ;
853+ Fantom . runTask ( ( ) => {
854+ root . render ( < MyApp /> ) ;
855+ } ) ;
856+
857+ const viewElement = ensureInstance ( viewRef . current , ReactNativeElement ) ;
858+
859+ Fantom . runTask ( ( ) => {
860+ Animated . spring ( _translateX , {
861+ toValue : 100 ,
862+ stiffness : 100 ,
863+ damping : 20 ,
864+ mass : 1 ,
865+ useNativeDriver : true ,
866+ } ) . start ( ) ;
867+ } ) ;
868+
869+ // Verify monotonic convergence (no oscillation)
870+ let prevValue = 0 ;
871+ for ( let i = 0 ; i < 10 ; i ++ ) {
872+ Fantom . unstable_produceFramesForDuration ( 200 ) ;
873+ const t =
874+ // $FlowFixMe[incompatible-use]
875+ Fantom . unstable_getDirectManipulationProps ( viewElement ) . transform [ 0 ] ;
876+ // Should approach 100 monotonically from below
877+ expect ( t . translateX ) . toBeGreaterThanOrEqual ( prevValue - 0.01 ) ;
878+ expect ( t . translateX ) . toBeLessThanOrEqual ( 100.01 ) ;
879+ prevValue = t . translateX ;
880+ }
881+
882+ Fantom . unstable_produceFramesForDuration ( 3000 ) ;
883+ Fantom . runWorkLoop ( ) ;
884+ expect ( viewElement . getBoundingClientRect ( ) . x ) . toBe ( 100 ) ;
885+ } ) ;
886+ } ) ;
0 commit comments