diff --git a/example/assets/rocket.riv b/example/assets/rocket.riv index dbc9042..303bf21 100644 Binary files a/example/assets/rocket.riv and b/example/assets/rocket.riv differ 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; }