From 1c7bccc36cf88fd0f76f2c8143ca554523c3e544 Mon Sep 17 00:00:00 2001 From: Luigi Rosso Date: Wed, 31 Mar 2021 16:16:15 -0700 Subject: [PATCH] Adding exit time to state machine. --- example/assets/rocket.riv | Bin 4939 -> 4598 bytes .../animation/state_transition_base.dart | 29 ++++++++++++- lib/src/generated/rive_core_context.dart | 11 +++++ .../rive_core/animation/linear_animation.dart | 4 ++ .../animation/linear_animation_instance.dart | 13 +++++- .../rive_core/animation/state_transition.dart | 35 ++++++++++++++++ .../animation/transition_bool_condition.dart | 2 + .../transition_double_condition.dart | 2 + .../transition_trigger_condition.dart | 2 + .../rive_core/state_machine_controller.dart | 38 ++++++++++++++++-- lib/src/rive_core/state_transition_flags.dart | 3 ++ 11 files changed, 133 insertions(+), 6 deletions(-) diff --git a/example/assets/rocket.riv b/example/assets/rocket.riv index dbc9042e92a17af20ca405b2bfa5067ff15fe06b..303bf211f1ad21c9b8c8d217763899be59d9c1d2 100644 GIT binary patch literal 4598 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 Q$VClpCdP?4tPzF$7rV{5mjD0& literal 4939 zcmb_f4Qx}_6~52pEiu6_HYDI@(!`M1$f<+;eTe zkkG9Ymbkg+eCK}moO92)?^1XFBQ@!4kCiQ8QaWSvt21ygv&eIkvsq+tGLyk?4i3yD zGwXvR^E4X{me1nDL8X4(!62I@ON#U4-4iTLHf0|g-W1Hifth6!v&*K^p@Ee;n=8w% z@WOZ;NF)nm5id;At;cURX*L{C^@h@WqF)EL3~Se!>pmL}7N*=YrWEZh3oxr}RwCiX zs8eDE@@zXgq~fs7p0+_}azf=CI<3RjW|j^A=E{mI4nHI%s{C-HAE>M;D%*4_p%PS9 zpt7?0a?YZ|znahs&<8e8p6?!u05MJ;X7P9t1fan->DJ<&?V1e-OEkx>D~0kqI&7$d z+p_GiM;cd6sHYF+#T?PL%0NODxQ$AmV#OB!t6*AOWqp~G%F$E5$D$~=MBe<~a%KO;(EyLiWB=KR5>wg!-}oUQk3D=NQ&;^L zM<-GeGc6kD61NQh^NNAz=b-~S=kZSA@lN3plHd`RoX0GeLng}MSbVO=?7zps{c_>{ zQ!mtLHXIao?)@l~XaFn&V2KTsZmsY8)Mvv%@i+egOSHy%wf?kb!@+W+EIh7Q%v#LJ z!10i%*o@y{7$N(fkH0>qHrW1!bzcK5S5wM9`yfyzEtPq(x{!7MxaFbh z5?y&O_(_ygtpTd=81zwi45)F_lXta*YV%)vA2Wh6A)LR2PrMD%bLy z)!Dt#@tBH(`JcEZ==$uSn-8IWtM9pJGJ0{gA0CmZA`l@oKLR~KgnVoRY6ibHxGsGFZ3p|) z#q!%iFK*ao9cX_}v*EzEZl|-_nY%dVh`OG~CTq9<RHJ^Y&y!(5*TM3m`A=UaQ0D zs?^lSUEwB<3RRdbhyE63eCnuhfXKKhZ+l{kIQL>S$R?)5#!aSXU&H{WiEwPHr$-9J3K(jOY1K;V}Q9lNv-dG8&i?Tg;!(0 zM!{)B78{X5BjPq9bWI61>MgSWfl$^2LRk|C zWlbQIHGxpp1VUL82xUzmlr@1+)&xRX69{EZAe1$MP}T%OSrZ6ljU%e37kaLbmGd5b z68_>1-M^y`x^W#KQ@)mX3x9zN`ObVTW$>aDHb$qPkwPnvKNoo| zj@e%^P>TQ`1VQ%AdVDXZEi z$zR1sGoSG{DqFEnN?+63*S-z#Rl^dz2)-kw;mm|H#(Yep38*BRfnH*MR#EvS{fX}G r&W``e>nCPT%Go4TNWhJo9g*7Decy$xV2}>eCQ?F)vO#YWdWro9b8g?q diff --git a/lib/src/generated/animation/state_transition_base.dart b/lib/src/generated/animation/state_transition_base.dart index 68bd390..eceb51a 100644 --- a/lib/src/generated/animation/state_transition_base.dart +++ b/lib/src/generated/animation/state_transition_base.dart @@ -65,7 +65,8 @@ abstract class StateTransitionBase extends StateMachineLayerComponent { int _duration = durationInitialValue; static const int durationPropertyKey = 158; - /// Duration of the trasition (mix time) in milliseconds. + /// Duration of the trasition (mix time) in milliseconds or percentage (0-100) + /// based on flags. int get duration => _duration; /// Change the [_duration] field value. @@ -82,4 +83,30 @@ abstract class StateTransitionBase extends StateMachineLayerComponent { } void durationChanged(int from, int to); + + /// -------------------------------------------------------------------------- + /// ExitTime field with key 160. + static const int exitTimeInitialValue = 0; + int _exitTime = exitTimeInitialValue; + static const int exitTimePropertyKey = 160; + + /// Duration in milliseconds that must elapse before allowing the state to + /// change. If the flags mark this property as being percentage based, the + /// value is in 0-100% of the outgoing animation's duration + int get exitTime => _exitTime; + + /// Change the [_exitTime] field value. + /// [exitTimeChanged] will be invoked only if the field's value has changed. + set exitTime(int value) { + if (_exitTime == value) { + return; + } + int from = _exitTime; + _exitTime = value; + if (hasValidated) { + exitTimeChanged(from, value); + } + } + + void exitTimeChanged(int from, int to); } diff --git a/lib/src/generated/rive_core_context.dart b/lib/src/generated/rive_core_context.dart index 097df3d..13c43bb 100644 --- a/lib/src/generated/rive_core_context.dart +++ b/lib/src/generated/rive_core_context.dart @@ -352,6 +352,11 @@ class RiveCoreContext { object.duration = value; } break; + case StateTransitionBase.exitTimePropertyKey: + if (object is StateTransitionBase && value is int) { + object.exitTime = value; + } + break; case KeyFrameDoubleBase.valuePropertyKey: if (object is KeyFrameDoubleBase && value is double) { object.value = value; @@ -831,6 +836,7 @@ class RiveCoreContext { case StateTransitionBase.stateToIdPropertyKey: case StateTransitionBase.flagsPropertyKey: case StateTransitionBase.durationPropertyKey: + case StateTransitionBase.exitTimePropertyKey: case LinearAnimationBase.fpsPropertyKey: case LinearAnimationBase.durationPropertyKey: case LinearAnimationBase.loopValuePropertyKey: @@ -980,6 +986,8 @@ class RiveCoreContext { return (object as StateTransitionBase).flags; case StateTransitionBase.durationPropertyKey: return (object as StateTransitionBase).duration; + case StateTransitionBase.exitTimePropertyKey: + return (object as StateTransitionBase).exitTime; case LinearAnimationBase.fpsPropertyKey: return (object as LinearAnimationBase).fps; case LinearAnimationBase.durationPropertyKey: @@ -1255,6 +1263,9 @@ class RiveCoreContext { case StateTransitionBase.durationPropertyKey: (object as StateTransitionBase).duration = value; break; + case StateTransitionBase.exitTimePropertyKey: + (object as StateTransitionBase).exitTime = value; + break; case LinearAnimationBase.fpsPropertyKey: (object as LinearAnimationBase).fps = value; break; diff --git a/lib/src/rive_core/animation/linear_animation.dart b/lib/src/rive_core/animation/linear_animation.dart index 9c6c239..df94bef 100644 --- a/lib/src/rive_core/animation/linear_animation.dart +++ b/lib/src/rive_core/animation/linear_animation.dart @@ -25,6 +25,10 @@ class LinearAnimation extends LinearAnimationBase { return true; } + double get startSeconds => (enableWorkArea ? workStart : 0).toDouble() / fps; + double get endSeconds => + (enableWorkArea ? workEnd : duration).toDouble() / fps; + double get durationSeconds => endSeconds - startSeconds; void apply(double time, {required CoreContext coreContext, double mix = 1}) { for (final keyedObject in _keyedObjects.values) { keyedObject.apply(time, mix, coreContext); diff --git a/lib/src/rive_core/animation/linear_animation_instance.dart b/lib/src/rive_core/animation/linear_animation_instance.dart index 9a7b461..14655b2 100644 --- a/lib/src/rive_core/animation/linear_animation_instance.dart +++ b/lib/src/rive_core/animation/linear_animation_instance.dart @@ -4,11 +4,15 @@ import 'package:rive/src/rive_core/animation/loop.dart'; class LinearAnimationInstance { final LinearAnimation animation; double _time = 0; + double _totalTime = 0; + double _lastTotalTime = 0; int _direction = 1; bool _didLoop = false; bool get didLoop => _didLoop; double _spilledTime = 0; double get spilledTime => _spilledTime; + double get totalTime => _totalTime; + double get lastTotalTime => _lastTotalTime; LinearAnimationInstance(this.animation) : _time = (animation.enableWorkArea ? animation.workStart : 0).toDouble() / @@ -17,7 +21,9 @@ class LinearAnimationInstance { if (_time == value) { return; } - _time = value; + var diff = _totalTime - _lastTotalTime; + _time = _totalTime = value; + _lastTotalTime = _totalTime - diff; _direction = 1; } @@ -35,7 +41,10 @@ class LinearAnimationInstance { void reset() => _time = startTime; bool get keepGoing => animation.loop != Loop.oneShot || !_didLoop; bool advance(double elapsedSeconds) { - _time += elapsedSeconds * animation.speed * _direction; + var deltaSeconds = elapsedSeconds * animation.speed * _direction; + _lastTotalTime = _totalTime; + _totalTime += deltaSeconds; + _time += deltaSeconds; double frames = _time * animation.fps; var start = animation.enableWorkArea ? animation.workStart : 0; var end = animation.enableWorkArea ? animation.workEnd : animation.duration; diff --git a/lib/src/rive_core/animation/state_transition.dart b/lib/src/rive_core/animation/state_transition.dart index 10a5ba0..077d7c7 100644 --- a/lib/src/rive_core/animation/state_transition.dart +++ b/lib/src/rive_core/animation/state_transition.dart @@ -1,4 +1,5 @@ import 'package:rive/src/core/core.dart'; +import 'package:rive/src/rive_core/animation/animation_state.dart'; import 'package:rive/src/rive_core/animation/layer_state.dart'; import 'package:rive/src/rive_core/animation/transition_condition.dart'; import 'package:rive/src/generated/animation/state_transition_base.dart'; @@ -24,6 +25,38 @@ class StateTransition extends StateTransitionBase { } bool get isDisabled => (flags & StateTransitionFlags.disabled) != 0; + bool get pauseOnExit => (flags & StateTransitionFlags.pauseOnExit) != 0; + bool get enableExitTime => (flags & StateTransitionFlags.enableExitTime) != 0; + double mixTime(LayerState stateFrom) { + if (duration == 0) { + return 0; + } + if ((flags & StateTransitionFlags.durationIsPercentage) != 0) { + var animationDuration = 0.0; + if (stateFrom is AnimationState) { + animationDuration = stateFrom.animation?.durationSeconds ?? 0; + } + return duration / 100 * animationDuration; + } else { + return duration / 1000; + } + } + + double exitTimeSeconds(LayerState stateFrom) { + if (exitTime == 0) { + return 0; + } + if ((flags & StateTransitionFlags.exitTimeIsPercentage) != 0) { + var animationDuration = 0.0; + if (stateFrom is AnimationState) { + animationDuration = stateFrom.animation?.durationSeconds ?? 0; + } + return exitTime / 100 * animationDuration; + } else { + return exitTime / 1000; + } + } + @override bool import(ImportStack importStack) { var importer = @@ -53,5 +86,7 @@ class StateTransition extends StateTransitionBase { @override void durationChanged(int from, int to) {} @override + void exitTimeChanged(int from, int to) {} + @override void stateToIdChanged(int from, int to) {} } diff --git a/lib/src/rive_core/animation/transition_bool_condition.dart b/lib/src/rive_core/animation/transition_bool_condition.dart index aafba6d..295b455 100644 --- a/lib/src/rive_core/animation/transition_bool_condition.dart +++ b/lib/src/rive_core/animation/transition_bool_condition.dart @@ -5,6 +5,8 @@ import 'package:rive/src/generated/animation/transition_bool_condition_base.dart export 'package:rive/src/generated/animation/transition_bool_condition_base.dart'; class TransitionBoolCondition extends TransitionBoolConditionBase { + @override + bool validate() => super.validate() && (input is StateMachineBool); @override bool evaluate(HashMap values) { var boolInput = input as StateMachineBool; diff --git a/lib/src/rive_core/animation/transition_double_condition.dart b/lib/src/rive_core/animation/transition_double_condition.dart index e823275..0c91529 100644 --- a/lib/src/rive_core/animation/transition_double_condition.dart +++ b/lib/src/rive_core/animation/transition_double_condition.dart @@ -8,6 +8,8 @@ class TransitionDoubleCondition extends TransitionDoubleConditionBase { @override void valueChanged(double from, double to) {} @override + bool validate() => super.validate() && (input is StateMachineDouble); + @override bool evaluate(HashMap values) { var doubleInput = input as StateMachineDouble; dynamic providedValue = values[input.id]; diff --git a/lib/src/rive_core/animation/transition_trigger_condition.dart b/lib/src/rive_core/animation/transition_trigger_condition.dart index 9d8dbe1..d1e4edd 100644 --- a/lib/src/rive_core/animation/transition_trigger_condition.dart +++ b/lib/src/rive_core/animation/transition_trigger_condition.dart @@ -4,6 +4,8 @@ import 'package:rive/src/generated/animation/transition_trigger_condition_base.d export 'package:rive/src/generated/animation/transition_trigger_condition_base.dart'; class TransitionTriggerCondition extends TransitionTriggerConditionBase { + @override + bool validate() => super.validate() && (input is StateMachineTrigger); @override bool evaluate(HashMap values) { dynamic providedValue = values[input.id]; diff --git a/lib/src/rive_core/state_machine_controller.dart b/lib/src/rive_core/state_machine_controller.dart index 16d7c5c..c5af8e3 100644 --- a/lib/src/rive_core/state_machine_controller.dart +++ b/lib/src/rive_core/state_machine_controller.dart @@ -11,6 +11,8 @@ import 'package:rive/src/rive_core/rive_animation_controller.dart'; class LayerController { final StateMachineLayer layer; LayerState? _currentState; + LayerState? _stateFrom; + bool _holdAnimationFrom = false; LinearAnimationInstance? _animationInstanceFrom; StateTransition? _transition; double _mix = 1.0; @@ -41,8 +43,10 @@ class LayerController { return false; } } - if (_transition != null && _transition!.duration != 0) { - _mix = (_mix + elapsedSeconds / (_transition!.duration / 1000)) + if (_transition != null && + _stateFrom != null && + _transition!.duration != 0) { + _mix = (_mix + elapsedSeconds / _transition!.mixTime(_stateFrom!)) .clamp(0, 1) .toDouble(); } else { @@ -50,7 +54,9 @@ class LayerController { } var keepGoing = _mix != 1; if (_animationInstanceFrom != null && _mix < 1) { - _animationInstanceFrom!.advance(elapsedSeconds); + if (!_holdAnimationFrom) { + _animationInstanceFrom!.advance(elapsedSeconds); + } _animationInstanceFrom!.animation.apply(_animationInstanceFrom!.time, mix: 1 - _mix, coreContext: core); } @@ -77,6 +83,9 @@ class LayerController { return false; } for (final transition in stateFrom.transitions) { + if (transition.isDisabled) { + continue; + } bool valid = true; for (final condition in transition.conditions) { if (!condition.evaluate(inputValues)) { @@ -84,9 +93,32 @@ class LayerController { break; } } + if (valid && stateFrom is AnimationState && transition.enableExitTime) { + var fromAnimation = stateFrom.animation!; + if (_animationInstance != null && + fromAnimation == _animationInstance!.animation) { + var lastTime = _animationInstance!.lastTotalTime; + var time = _animationInstance!.totalTime; + var exitTime = transition.exitTimeSeconds(stateFrom); + if (exitTime < fromAnimation.durationSeconds) { + exitTime += (lastTime / fromAnimation.durationSeconds).floor() * + fromAnimation.durationSeconds; + } + if (time < exitTime) { + valid = false; + } + } + } if (valid && _changeState(transition.stateTo)) { _transition = transition; + _stateFrom = stateFrom; + if (transition.pauseOnExit && + transition.enableExitTime && + _animationInstance != null) { + _animationInstance!.time = transition.exitTimeSeconds(stateFrom); + } if (_mix != 0) { + _holdAnimationFrom = transition.pauseOnExit; _animationInstanceFrom = _animationInstance; } if (_currentState is AnimationState) { diff --git a/lib/src/rive_core/state_transition_flags.dart b/lib/src/rive_core/state_transition_flags.dart index b917ed4..b6f0afd 100644 --- a/lib/src/rive_core/state_transition_flags.dart +++ b/lib/src/rive_core/state_transition_flags.dart @@ -1,4 +1,7 @@ class StateTransitionFlags { static const int disabled = 1 << 0; static const int durationIsPercentage = 1 << 1; + static const int enableExitTime = 1 << 2; + static const int exitTimeIsPercentage = 1 << 3; + static const int pauseOnExit = 1 << 4; }