diff --git a/example/assets/liquid_download.riv b/example/assets/liquid_download.riv new file mode 100644 index 0000000..8c933a4 Binary files /dev/null and b/example/assets/liquid_download.riv differ diff --git a/example/lib/liquid_download.dart b/example/lib/liquid_download.dart new file mode 100644 index 0000000..3957677 --- /dev/null +++ b/example/lib/liquid_download.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:rive/rive.dart'; + +/// An example showing how to drive a StateMachine via a trigger and number +/// input. +class LiquidDownload extends StatefulWidget { + const LiquidDownload({Key? key}) : super(key: key); + + @override + _LiquidDownloadState createState() => _LiquidDownloadState(); +} + +class _LiquidDownloadState extends State { + /// Tracks if the animation is playing by whether controller is running. + bool get isPlaying => _controller?.isActive ?? false; + + Artboard? _riveArtboard; + StateMachineController? _controller; + SMIInput? _start; + SMIInput? _progress; + + @override + void initState() { + super.initState(); + + // Load the animation file from the bundle, note that you could also + // download this. The RiveFile just expects a list of bytes. + rootBundle.load('assets/liquid_download.riv').then( + (data) async { + // Load the RiveFile from the binary data. + final file = RiveFile.import(data); + + // The artboard is the root of the animation and gets drawn in the + // Rive widget. + final artboard = file.mainArtboard; + var controller = + StateMachineController.fromArtboard(artboard, 'Download'); + if (controller != null) { + artboard.addController(controller); + _start = controller.findInput('Download'); + _progress = controller.findInput('Progress'); + } + setState(() => _riveArtboard = artboard); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey, + appBar: AppBar( + title: const Text('Liquid Download'), + ), + body: Center( + child: _riveArtboard == null + ? const SizedBox() + : GestureDetector( + onTapDown: (_) => _start?.value = true, + child: Column( + children: [ + const SizedBox(height: 10), + const Text( + 'Press to activate, slide for progress...', + style: TextStyle( + fontSize: 18, + ), + ), + Slider( + value: _progress!.value, + min: 0, + max: 100, + label: _progress!.value.round().toString(), + onChanged: (double value) { + setState(() { + _progress!.value = value; + }); + }, + ), + const SizedBox(height: 10), + Expanded( + child: Rive( + artboard: _riveArtboard!, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 2434d5e..27fe750 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:rive_example/example_animation.dart'; import 'package:rive_example/example_state_machine.dart'; +import 'package:rive_example/liquid_download.dart'; import 'package:rive_example/little_machine.dart'; import 'package:rive_example/state_machine_skills.dart'; @@ -73,6 +74,20 @@ class Home extends StatelessWidget { ); }, ), + const SizedBox( + height: 10, + ), + ElevatedButton( + child: const Text('Liquid Download'), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const LiquidDownload(), + ), + ); + }, + ), ], ), ), diff --git a/lib/src/blend_animations.dart b/lib/src/blend_animations.dart new file mode 100644 index 0000000..8566e82 --- /dev/null +++ b/lib/src/blend_animations.dart @@ -0,0 +1,20 @@ +import 'dart:collection'; + +import 'package:rive/src/rive_core/animation/blend_animation.dart'; + +class BlendAnimations extends ListBase { + final List _values = []; + List get values => _values.cast(); + + @override + int get length => _values.length; + + @override + set length(int value) => _values.length = value; + + @override + T operator [](int index) => _values[index]!; + + @override + void operator []=(int index, T value) => _values[index] = value; +} diff --git a/lib/src/core/core.dart b/lib/src/core/core.dart index 330f3b6..ed8a5d1 100644 --- a/lib/src/core/core.dart +++ b/lib/src/core/core.dart @@ -3,6 +3,7 @@ import 'dart:collection'; import 'package:rive/src/rive_core/runtime/exceptions/rive_format_error_exception.dart'; export 'package:rive/src/animation_list.dart'; export 'package:rive/src/state_machine_components.dart'; +export 'package:rive/src/blend_animations.dart'; export 'package:rive/src/state_transition_conditions.dart'; export 'package:rive/src/state_transitions.dart'; export 'package:rive/src/container_children.dart'; diff --git a/lib/src/core/importers/layer_state_importer.dart b/lib/src/core/importers/layer_state_importer.dart index e06088c..3dd8e8f 100644 --- a/lib/src/core/importers/layer_state_importer.dart +++ b/lib/src/core/importers/layer_state_importer.dart @@ -1,4 +1,8 @@ import 'package:rive/src/core/importers/artboard_import_stack_object.dart'; +import 'package:rive/src/rive_core/animation/blend_animation.dart'; +import 'package:rive/src/rive_core/animation/blend_state.dart'; +import 'package:rive/src/rive_core/animation/blend_state_direct.dart'; +import 'package:rive/src/rive_core/animation/blend_state_transition.dart'; import 'package:rive/src/rive_core/animation/layer_state.dart'; import 'package:rive/src/rive_core/animation/state_transition.dart'; @@ -10,4 +14,24 @@ class LayerStateImporter extends ArtboardImportStackObject { state.context.addObject(transition); state.internalAddTransition(transition); } + + bool addBlendAnimation(BlendAnimation blendAnimation) { + if (state is BlendStateDirect) { + var blendState = state as BlendStateDirect; + for (final transition + in state.transitions.whereType()) { + if (transition.exitBlendAnimationId >= 0 && + transition.exitBlendAnimationId < blendState.animations.length) { + transition.exitBlendAnimation = + blendState.animations[transition.exitBlendAnimationId]; + } + } + } + if (state is BlendState) { + (state as BlendState).internalAddAnimation(blendAnimation); + return true; + } + + return false; + } } diff --git a/lib/src/core/importers/state_machine_layer_importer.dart b/lib/src/core/importers/state_machine_layer_importer.dart index 1c4f56f..fbe7e48 100644 --- a/lib/src/core/importers/state_machine_layer_importer.dart +++ b/lib/src/core/importers/state_machine_layer_importer.dart @@ -1,13 +1,11 @@ import 'package:rive/rive.dart'; 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/state_machine_layer.dart'; class StateMachineLayerImporter extends ImportStackObject { final StateMachineLayer layer; - final ArtboardImporter artboardImporter; - StateMachineLayerImporter(this.layer, this.artboardImporter); + StateMachineLayerImporter(this.layer); final List importedStates = []; @@ -27,12 +25,6 @@ class StateMachineLayerImporter extends ImportStackObject { assert(!_resolved); _resolved = true; for (final state in importedStates) { - if (state is AnimationState) { - int artboardAnimationIndex = state.animationId; - assert(artboardAnimationIndex >= 0 && - artboardAnimationIndex < artboardImporter.animations.length); - state.animation = artboardImporter.animations[artboardAnimationIndex]; - } for (final transition in state.transitions) { // At import time the stateToId is an index relative to the entire layer // (which state in this layer). We can use that to find the matching diff --git a/lib/src/generated/animation/blend_animation_1d_base.dart b/lib/src/generated/animation/blend_animation_1d_base.dart new file mode 100644 index 0000000..1c5a102 --- /dev/null +++ b/lib/src/generated/animation/blend_animation_1d_base.dart @@ -0,0 +1,37 @@ +/// Core automatically generated +/// lib/src/generated/animation/blend_animation_1d_base.dart. +/// Do not modify manually. + +import 'package:rive/src/generated/animation/blend_animation_base.dart'; +import 'package:rive/src/rive_core/animation/blend_animation.dart'; + +abstract class BlendAnimation1DBase extends BlendAnimation { + static const int typeKey = 75; + @override + int get coreType => BlendAnimation1DBase.typeKey; + @override + Set get coreTypes => + {BlendAnimation1DBase.typeKey, BlendAnimationBase.typeKey}; + + /// -------------------------------------------------------------------------- + /// Value field with key 166. + static const double valueInitialValue = 0; + double _value = valueInitialValue; + static const int valuePropertyKey = 166; + double get value => _value; + + /// Change the [_value] field value. + /// [valueChanged] will be invoked only if the field's value has changed. + set value(double value) { + if (_value == value) { + return; + } + double from = _value; + _value = value; + if (hasValidated) { + valueChanged(from, value); + } + } + + void valueChanged(double from, double to); +} diff --git a/lib/src/generated/animation/blend_animation_base.dart b/lib/src/generated/animation/blend_animation_base.dart new file mode 100644 index 0000000..984c39b --- /dev/null +++ b/lib/src/generated/animation/blend_animation_base.dart @@ -0,0 +1,38 @@ +/// Core automatically generated +/// lib/src/generated/animation/blend_animation_base.dart. +/// Do not modify manually. + +import 'package:rive/src/core/core.dart'; + +abstract class BlendAnimationBase extends Core { + static const int typeKey = 74; + @override + int get coreType => BlendAnimationBase.typeKey; + @override + Set get coreTypes => {BlendAnimationBase.typeKey}; + + /// -------------------------------------------------------------------------- + /// AnimationId field with key 165. + static const int animationIdInitialValue = -1; + int _animationId = animationIdInitialValue; + static const int animationIdPropertyKey = 165; + + /// Id of the animation this BlendAnimation references. + int get animationId => _animationId; + + /// Change the [_animationId] field value. + /// [animationIdChanged] will be invoked only if the field's value has + /// changed. + set animationId(int value) { + if (_animationId == value) { + return; + } + int from = _animationId; + _animationId = value; + if (hasValidated) { + animationIdChanged(from, value); + } + } + + void animationIdChanged(int from, int to); +} diff --git a/lib/src/generated/animation/blend_animation_direct_base.dart b/lib/src/generated/animation/blend_animation_direct_base.dart new file mode 100644 index 0000000..12feb5b --- /dev/null +++ b/lib/src/generated/animation/blend_animation_direct_base.dart @@ -0,0 +1,39 @@ +/// Core automatically generated +/// lib/src/generated/animation/blend_animation_direct_base.dart. +/// Do not modify manually. + +import 'package:rive/src/generated/animation/blend_animation_base.dart'; +import 'package:rive/src/rive_core/animation/blend_animation.dart'; + +abstract class BlendAnimationDirectBase extends BlendAnimation { + static const int typeKey = 77; + @override + int get coreType => BlendAnimationDirectBase.typeKey; + @override + Set get coreTypes => + {BlendAnimationDirectBase.typeKey, BlendAnimationBase.typeKey}; + + /// -------------------------------------------------------------------------- + /// InputId field with key 168. + static const int inputIdInitialValue = -1; + int _inputId = inputIdInitialValue; + static const int inputIdPropertyKey = 168; + + /// Id of the input that drives the direct mix value for this animation. + int get inputId => _inputId; + + /// Change the [_inputId] field value. + /// [inputIdChanged] will be invoked only if the field's value has changed. + set inputId(int value) { + if (_inputId == value) { + return; + } + int from = _inputId; + _inputId = value; + if (hasValidated) { + inputIdChanged(from, value); + } + } + + void inputIdChanged(int from, int to); +} diff --git a/lib/src/generated/animation/blend_state_1d_base.dart b/lib/src/generated/animation/blend_state_1d_base.dart new file mode 100644 index 0000000..c917de4 --- /dev/null +++ b/lib/src/generated/animation/blend_state_1d_base.dart @@ -0,0 +1,46 @@ +/// Core automatically generated +/// lib/src/generated/animation/blend_state_1d_base.dart. +/// Do not modify manually. + +import 'package:rive/src/generated/animation/blend_state_base.dart'; +import 'package:rive/src/generated/animation/layer_state_base.dart'; +import 'package:rive/src/generated/animation/state_machine_layer_component_base.dart'; +import 'package:rive/src/rive_core/animation/blend_animation_1d.dart'; +import 'package:rive/src/rive_core/animation/blend_state.dart'; + +abstract class BlendState1DBase extends BlendState { + static const int typeKey = 76; + @override + int get coreType => BlendState1DBase.typeKey; + @override + Set get coreTypes => { + BlendState1DBase.typeKey, + BlendStateBase.typeKey, + LayerStateBase.typeKey, + StateMachineLayerComponentBase.typeKey + }; + + /// -------------------------------------------------------------------------- + /// InputId field with key 167. + static const int inputIdInitialValue = -1; + int _inputId = inputIdInitialValue; + static const int inputIdPropertyKey = 167; + + /// Id of the input that drives the mix value for this blend state. + int get inputId => _inputId; + + /// Change the [_inputId] field value. + /// [inputIdChanged] will be invoked only if the field's value has changed. + set inputId(int value) { + if (_inputId == value) { + return; + } + int from = _inputId; + _inputId = value; + if (hasValidated) { + inputIdChanged(from, value); + } + } + + void inputIdChanged(int from, int to); +} diff --git a/lib/src/generated/animation/blend_state_base.dart b/lib/src/generated/animation/blend_state_base.dart new file mode 100644 index 0000000..1f97b13 --- /dev/null +++ b/lib/src/generated/animation/blend_state_base.dart @@ -0,0 +1,19 @@ +/// Core automatically generated +/// lib/src/generated/animation/blend_state_base.dart. +/// Do not modify manually. + +import 'package:rive/src/generated/animation/layer_state_base.dart'; +import 'package:rive/src/generated/animation/state_machine_layer_component_base.dart'; +import 'package:rive/src/rive_core/animation/layer_state.dart'; + +abstract class BlendStateBase extends LayerState { + static const int typeKey = 72; + @override + int get coreType => BlendStateBase.typeKey; + @override + Set get coreTypes => { + BlendStateBase.typeKey, + LayerStateBase.typeKey, + StateMachineLayerComponentBase.typeKey + }; +} diff --git a/lib/src/generated/animation/blend_state_direct_base.dart b/lib/src/generated/animation/blend_state_direct_base.dart new file mode 100644 index 0000000..684af45 --- /dev/null +++ b/lib/src/generated/animation/blend_state_direct_base.dart @@ -0,0 +1,22 @@ +/// Core automatically generated +/// lib/src/generated/animation/blend_state_direct_base.dart. +/// Do not modify manually. + +import 'package:rive/src/generated/animation/blend_state_base.dart'; +import 'package:rive/src/generated/animation/layer_state_base.dart'; +import 'package:rive/src/generated/animation/state_machine_layer_component_base.dart'; +import 'package:rive/src/rive_core/animation/blend_animation_direct.dart'; +import 'package:rive/src/rive_core/animation/blend_state.dart'; + +abstract class BlendStateDirectBase extends BlendState { + static const int typeKey = 73; + @override + int get coreType => BlendStateDirectBase.typeKey; + @override + Set get coreTypes => { + BlendStateDirectBase.typeKey, + BlendStateBase.typeKey, + LayerStateBase.typeKey, + StateMachineLayerComponentBase.typeKey + }; +} diff --git a/lib/src/generated/animation/blend_state_transition_base.dart b/lib/src/generated/animation/blend_state_transition_base.dart new file mode 100644 index 0000000..8d11f38 --- /dev/null +++ b/lib/src/generated/animation/blend_state_transition_base.dart @@ -0,0 +1,44 @@ +/// Core automatically generated +/// lib/src/generated/animation/blend_state_transition_base.dart. +/// Do not modify manually. + +import 'package:rive/src/generated/animation/state_machine_layer_component_base.dart'; +import 'package:rive/src/generated/animation/state_transition_base.dart'; +import 'package:rive/src/rive_core/animation/state_transition.dart'; + +abstract class BlendStateTransitionBase extends StateTransition { + static const int typeKey = 78; + @override + int get coreType => BlendStateTransitionBase.typeKey; + @override + Set get coreTypes => { + BlendStateTransitionBase.typeKey, + StateTransitionBase.typeKey, + StateMachineLayerComponentBase.typeKey + }; + + /// -------------------------------------------------------------------------- + /// ExitBlendAnimationId field with key 171. + static const int exitBlendAnimationIdInitialValue = -1; + int _exitBlendAnimationId = exitBlendAnimationIdInitialValue; + static const int exitBlendAnimationIdPropertyKey = 171; + + /// Id of the state the blend state animation used for exit time calculation. + int get exitBlendAnimationId => _exitBlendAnimationId; + + /// Change the [_exitBlendAnimationId] field value. + /// [exitBlendAnimationIdChanged] will be invoked only if the field's value + /// has changed. + set exitBlendAnimationId(int value) { + if (_exitBlendAnimationId == value) { + return; + } + int from = _exitBlendAnimationId; + _exitBlendAnimationId = value; + if (hasValidated) { + exitBlendAnimationIdChanged(from, value); + } + } + + void exitBlendAnimationIdChanged(int from, int to); +} diff --git a/lib/src/generated/rive_core_context.dart b/lib/src/generated/rive_core_context.dart index fb6dd91..7645d98 100644 --- a/lib/src/generated/rive_core_context.dart +++ b/lib/src/generated/rive_core_context.dart @@ -8,6 +8,12 @@ import 'package:rive/src/core/field_types/core_uint_type.dart'; import 'package:rive/src/generated/animation/animation_base.dart'; import 'package:rive/src/generated/animation/animation_state_base.dart'; import 'package:rive/src/generated/animation/any_state_base.dart'; +import 'package:rive/src/generated/animation/blend_animation_1d_base.dart'; +import 'package:rive/src/generated/animation/blend_animation_base.dart'; +import 'package:rive/src/generated/animation/blend_animation_direct_base.dart'; +import 'package:rive/src/generated/animation/blend_state_1d_base.dart'; +import 'package:rive/src/generated/animation/blend_state_direct_base.dart'; +import 'package:rive/src/generated/animation/blend_state_transition_base.dart'; import 'package:rive/src/generated/animation/cubic_interpolator_base.dart'; import 'package:rive/src/generated/animation/entry_state_base.dart'; import 'package:rive/src/generated/animation/exit_state_base.dart'; @@ -70,6 +76,11 @@ import 'package:rive/src/generated/transform_component_base.dart'; import 'package:rive/src/rive_core/animation/animation.dart'; import 'package:rive/src/rive_core/animation/animation_state.dart'; import 'package:rive/src/rive_core/animation/any_state.dart'; +import 'package:rive/src/rive_core/animation/blend_animation_1d.dart'; +import 'package:rive/src/rive_core/animation/blend_animation_direct.dart'; +import 'package:rive/src/rive_core/animation/blend_state_1d.dart'; +import 'package:rive/src/rive_core/animation/blend_state_direct.dart'; +import 'package:rive/src/rive_core/animation/blend_state_transition.dart'; import 'package:rive/src/rive_core/animation/cubic_interpolator.dart'; import 'package:rive/src/rive_core/animation/entry_state.dart'; import 'package:rive/src/rive_core/animation/exit_state.dart'; @@ -129,6 +140,8 @@ class RiveCoreContext { return AnimationState(); case KeyedObjectBase.typeKey: return KeyedObject(); + case BlendAnimationDirectBase.typeKey: + return BlendAnimationDirect(); case StateMachineNumberBase.typeKey: return StateMachineNumber(); case TransitionTriggerConditionBase.typeKey: @@ -161,10 +174,18 @@ class RiveCoreContext { return LinearAnimation(); case StateMachineTriggerBase.typeKey: return StateMachineTrigger(); + case BlendStateDirectBase.typeKey: + return BlendStateDirect(); case ExitStateBase.typeKey: return ExitState(); + case BlendAnimation1DBase.typeKey: + return BlendAnimation1D(); + case BlendState1DBase.typeKey: + return BlendState1D(); case TransitionBoolConditionBase.typeKey: return TransitionBoolCondition(); + case BlendStateTransitionBase.typeKey: + return BlendStateTransition(); case StateMachineBoolBase.typeKey: return StateMachineBool(); case LinearGradientBase.typeKey: @@ -262,6 +283,16 @@ class RiveCoreContext { object.objectId = value; } break; + case BlendAnimationBase.animationIdPropertyKey: + if (object is BlendAnimationBase && value is int) { + object.animationId = value; + } + break; + case BlendAnimationDirectBase.inputIdPropertyKey: + if (object is BlendAnimationDirectBase && value is int) { + object.inputId = value; + } + break; case StateMachineComponentBase.namePropertyKey: if (object is StateMachineComponentBase && value is String) { object.name = value; @@ -402,6 +433,21 @@ class RiveCoreContext { object.enableWorkArea = value; } break; + case BlendAnimation1DBase.valuePropertyKey: + if (object is BlendAnimation1DBase && value is double) { + object.value = value; + } + break; + case BlendState1DBase.inputIdPropertyKey: + if (object is BlendState1DBase && value is int) { + object.inputId = value; + } + break; + case BlendStateTransitionBase.exitBlendAnimationIdPropertyKey: + if (object is BlendStateTransitionBase && value is int) { + object.exitBlendAnimationId = value; + } + break; case StateMachineBoolBase.valuePropertyKey: if (object is StateMachineBoolBase && value is bool) { object.value = value; @@ -846,6 +892,8 @@ class RiveCoreContext { case DrawTargetBase.placementValuePropertyKey: case AnimationStateBase.animationIdPropertyKey: case KeyedObjectBase.objectIdPropertyKey: + case BlendAnimationBase.animationIdPropertyKey: + case BlendAnimationDirectBase.inputIdPropertyKey: case TransitionConditionBase.inputIdPropertyKey: case KeyedPropertyBase.propertyKeyPropertyKey: case KeyFrameBase.framePropertyKey: @@ -862,6 +910,8 @@ class RiveCoreContext { case LinearAnimationBase.loopValuePropertyKey: case LinearAnimationBase.workStartPropertyKey: case LinearAnimationBase.workEndPropertyKey: + case BlendState1DBase.inputIdPropertyKey: + case BlendStateTransitionBase.exitBlendAnimationIdPropertyKey: case StrokeBase.capPropertyKey: case StrokeBase.joinPropertyKey: case TrimPathBase.modeValuePropertyKey: @@ -889,6 +939,7 @@ class RiveCoreContext { case CubicInterpolatorBase.y2PropertyKey: case KeyFrameDoubleBase.valuePropertyKey: case LinearAnimationBase.speedPropertyKey: + case BlendAnimation1DBase.valuePropertyKey: case LinearGradientBase.startXPropertyKey: case LinearGradientBase.startYPropertyKey: case LinearGradientBase.endXPropertyKey: @@ -990,6 +1041,10 @@ class RiveCoreContext { return (object as AnimationStateBase).animationId; case KeyedObjectBase.objectIdPropertyKey: return (object as KeyedObjectBase).objectId; + case BlendAnimationBase.animationIdPropertyKey: + return (object as BlendAnimationBase).animationId; + case BlendAnimationDirectBase.inputIdPropertyKey: + return (object as BlendAnimationDirectBase).inputId; case TransitionConditionBase.inputIdPropertyKey: return (object as TransitionConditionBase).inputId; case KeyedPropertyBase.propertyKeyPropertyKey: @@ -1022,6 +1077,10 @@ class RiveCoreContext { return (object as LinearAnimationBase).workStart; case LinearAnimationBase.workEndPropertyKey: return (object as LinearAnimationBase).workEnd; + case BlendState1DBase.inputIdPropertyKey: + return (object as BlendState1DBase).inputId; + case BlendStateTransitionBase.exitBlendAnimationIdPropertyKey: + return (object as BlendStateTransitionBase).exitBlendAnimationId; case StrokeBase.capPropertyKey: return (object as StrokeBase).cap; case StrokeBase.joinPropertyKey: @@ -1080,6 +1139,8 @@ class RiveCoreContext { return (object as KeyFrameDoubleBase).value; case LinearAnimationBase.speedPropertyKey: return (object as LinearAnimationBase).speed; + case BlendAnimation1DBase.valuePropertyKey: + return (object as BlendAnimation1DBase).value; case LinearGradientBase.startXPropertyKey: return (object as LinearGradientBase).startX; case LinearGradientBase.startYPropertyKey: @@ -1237,13 +1298,19 @@ class RiveCoreContext { static void setString(Core object, int propertyKey, String value) { switch (propertyKey) { case ComponentBase.namePropertyKey: - if (object is ComponentBase) object.name = value; + if (object is ComponentBase) { + object.name = value; + } break; case StateMachineComponentBase.namePropertyKey: - if (object is StateMachineComponentBase) object.name = value; + if (object is StateMachineComponentBase) { + object.name = value; + } break; case AnimationBase.namePropertyKey: - if (object is AnimationBase) object.name = value; + if (object is AnimationBase) { + object.name = value; + } break; } } @@ -1251,121 +1318,219 @@ class RiveCoreContext { static void setUint(Core object, int propertyKey, int value) { switch (propertyKey) { case ComponentBase.parentIdPropertyKey: - if (object is ComponentBase) object.parentId = value; + if (object is ComponentBase) { + object.parentId = value; + } break; case DrawTargetBase.drawableIdPropertyKey: - if (object is DrawTargetBase) object.drawableId = value; + if (object is DrawTargetBase) { + object.drawableId = value; + } break; case DrawTargetBase.placementValuePropertyKey: - if (object is DrawTargetBase) object.placementValue = value; + if (object is DrawTargetBase) { + object.placementValue = value; + } break; case AnimationStateBase.animationIdPropertyKey: - if (object is AnimationStateBase) object.animationId = value; + if (object is AnimationStateBase) { + object.animationId = value; + } break; case KeyedObjectBase.objectIdPropertyKey: - if (object is KeyedObjectBase) object.objectId = value; + if (object is KeyedObjectBase) { + object.objectId = value; + } + break; + case BlendAnimationBase.animationIdPropertyKey: + if (object is BlendAnimationBase) { + object.animationId = value; + } + break; + case BlendAnimationDirectBase.inputIdPropertyKey: + if (object is BlendAnimationDirectBase) { + object.inputId = value; + } break; case TransitionConditionBase.inputIdPropertyKey: - if (object is TransitionConditionBase) object.inputId = value; + if (object is TransitionConditionBase) { + object.inputId = value; + } break; case KeyedPropertyBase.propertyKeyPropertyKey: - if (object is KeyedPropertyBase) object.propertyKey = value; + if (object is KeyedPropertyBase) { + object.propertyKey = value; + } break; case KeyFrameBase.framePropertyKey: - if (object is KeyFrameBase) object.frame = value; + if (object is KeyFrameBase) { + object.frame = value; + } break; case KeyFrameBase.interpolationTypePropertyKey: - if (object is KeyFrameBase) object.interpolationType = value; + if (object is KeyFrameBase) { + object.interpolationType = value; + } break; case KeyFrameBase.interpolatorIdPropertyKey: - if (object is KeyFrameBase) object.interpolatorId = value; + if (object is KeyFrameBase) { + object.interpolatorId = value; + } break; case KeyFrameIdBase.valuePropertyKey: - if (object is KeyFrameIdBase) object.value = value; + if (object is KeyFrameIdBase) { + object.value = value; + } break; case TransitionValueConditionBase.opValuePropertyKey: - if (object is TransitionValueConditionBase) object.opValue = value; + if (object is TransitionValueConditionBase) { + object.opValue = value; + } break; case StateTransitionBase.stateToIdPropertyKey: - if (object is StateTransitionBase) object.stateToId = value; + if (object is StateTransitionBase) { + object.stateToId = value; + } break; case StateTransitionBase.flagsPropertyKey: - if (object is StateTransitionBase) object.flags = value; + if (object is StateTransitionBase) { + object.flags = value; + } break; case StateTransitionBase.durationPropertyKey: - if (object is StateTransitionBase) object.duration = value; + if (object is StateTransitionBase) { + object.duration = value; + } break; case StateTransitionBase.exitTimePropertyKey: - if (object is StateTransitionBase) object.exitTime = value; + if (object is StateTransitionBase) { + object.exitTime = value; + } break; case LinearAnimationBase.fpsPropertyKey: - if (object is LinearAnimationBase) object.fps = value; + if (object is LinearAnimationBase) { + object.fps = value; + } break; case LinearAnimationBase.durationPropertyKey: - if (object is LinearAnimationBase) object.duration = value; + if (object is LinearAnimationBase) { + object.duration = value; + } break; case LinearAnimationBase.loopValuePropertyKey: - if (object is LinearAnimationBase) object.loopValue = value; + if (object is LinearAnimationBase) { + object.loopValue = value; + } break; case LinearAnimationBase.workStartPropertyKey: - if (object is LinearAnimationBase) object.workStart = value; + if (object is LinearAnimationBase) { + object.workStart = value; + } break; case LinearAnimationBase.workEndPropertyKey: - if (object is LinearAnimationBase) object.workEnd = value; + if (object is LinearAnimationBase) { + object.workEnd = value; + } + break; + case BlendState1DBase.inputIdPropertyKey: + if (object is BlendState1DBase) { + object.inputId = value; + } + break; + case BlendStateTransitionBase.exitBlendAnimationIdPropertyKey: + if (object is BlendStateTransitionBase) { + object.exitBlendAnimationId = value; + } break; case StrokeBase.capPropertyKey: - if (object is StrokeBase) object.cap = value; + if (object is StrokeBase) { + object.cap = value; + } break; case StrokeBase.joinPropertyKey: - if (object is StrokeBase) object.join = value; + if (object is StrokeBase) { + object.join = value; + } break; case TrimPathBase.modeValuePropertyKey: - if (object is TrimPathBase) object.modeValue = value; + if (object is TrimPathBase) { + object.modeValue = value; + } break; case FillBase.fillRulePropertyKey: - if (object is FillBase) object.fillRule = value; + if (object is FillBase) { + object.fillRule = value; + } break; case PathBase.pathFlagsPropertyKey: - if (object is PathBase) object.pathFlags = value; + if (object is PathBase) { + object.pathFlags = value; + } break; case DrawableBase.blendModeValuePropertyKey: - if (object is DrawableBase) object.blendModeValue = value; + if (object is DrawableBase) { + object.blendModeValue = value; + } break; case DrawableBase.drawableFlagsPropertyKey: - if (object is DrawableBase) object.drawableFlags = value; + if (object is DrawableBase) { + object.drawableFlags = value; + } break; case WeightBase.valuesPropertyKey: - if (object is WeightBase) object.values = value; + if (object is WeightBase) { + object.values = value; + } break; case WeightBase.indicesPropertyKey: - if (object is WeightBase) object.indices = value; + if (object is WeightBase) { + object.indices = value; + } break; case CubicWeightBase.inValuesPropertyKey: - if (object is CubicWeightBase) object.inValues = value; + if (object is CubicWeightBase) { + object.inValues = value; + } break; case CubicWeightBase.inIndicesPropertyKey: - if (object is CubicWeightBase) object.inIndices = value; + if (object is CubicWeightBase) { + object.inIndices = value; + } break; case CubicWeightBase.outValuesPropertyKey: - if (object is CubicWeightBase) object.outValues = value; + if (object is CubicWeightBase) { + object.outValues = value; + } break; case CubicWeightBase.outIndicesPropertyKey: - if (object is CubicWeightBase) object.outIndices = value; + if (object is CubicWeightBase) { + object.outIndices = value; + } break; case ClippingShapeBase.sourceIdPropertyKey: - if (object is ClippingShapeBase) object.sourceId = value; + if (object is ClippingShapeBase) { + object.sourceId = value; + } break; case ClippingShapeBase.fillRulePropertyKey: - if (object is ClippingShapeBase) object.fillRule = value; + if (object is ClippingShapeBase) { + object.fillRule = value; + } break; case PolygonBase.pointsPropertyKey: - if (object is PolygonBase) object.points = value; + if (object is PolygonBase) { + object.points = value; + } break; case DrawRulesBase.drawTargetIdPropertyKey: - if (object is DrawRulesBase) object.drawTargetId = value; + if (object is DrawRulesBase) { + object.drawTargetId = value; + } break; case TendonBase.boneIdPropertyKey: - if (object is TendonBase) object.boneId = value; + if (object is TendonBase) { + object.boneId = value; + } break; } } @@ -1373,205 +1538,344 @@ class RiveCoreContext { static void setDouble(Core object, int propertyKey, double value) { switch (propertyKey) { case StateMachineNumberBase.valuePropertyKey: - if (object is StateMachineNumberBase) object.value = value; + if (object is StateMachineNumberBase) { + object.value = value; + } break; case TransitionNumberConditionBase.valuePropertyKey: - if (object is TransitionNumberConditionBase) object.value = value; + if (object is TransitionNumberConditionBase) { + object.value = value; + } break; case CubicInterpolatorBase.x1PropertyKey: - if (object is CubicInterpolatorBase) object.x1 = value; + if (object is CubicInterpolatorBase) { + object.x1 = value; + } break; case CubicInterpolatorBase.y1PropertyKey: - if (object is CubicInterpolatorBase) object.y1 = value; + if (object is CubicInterpolatorBase) { + object.y1 = value; + } break; case CubicInterpolatorBase.x2PropertyKey: - if (object is CubicInterpolatorBase) object.x2 = value; + if (object is CubicInterpolatorBase) { + object.x2 = value; + } break; case CubicInterpolatorBase.y2PropertyKey: - if (object is CubicInterpolatorBase) object.y2 = value; + if (object is CubicInterpolatorBase) { + object.y2 = value; + } break; case KeyFrameDoubleBase.valuePropertyKey: - if (object is KeyFrameDoubleBase) object.value = value; + if (object is KeyFrameDoubleBase) { + object.value = value; + } break; case LinearAnimationBase.speedPropertyKey: - if (object is LinearAnimationBase) object.speed = value; + if (object is LinearAnimationBase) { + object.speed = value; + } + break; + case BlendAnimation1DBase.valuePropertyKey: + if (object is BlendAnimation1DBase) { + object.value = value; + } break; case LinearGradientBase.startXPropertyKey: - if (object is LinearGradientBase) object.startX = value; + if (object is LinearGradientBase) { + object.startX = value; + } break; case LinearGradientBase.startYPropertyKey: - if (object is LinearGradientBase) object.startY = value; + if (object is LinearGradientBase) { + object.startY = value; + } break; case LinearGradientBase.endXPropertyKey: - if (object is LinearGradientBase) object.endX = value; + if (object is LinearGradientBase) { + object.endX = value; + } break; case LinearGradientBase.endYPropertyKey: - if (object is LinearGradientBase) object.endY = value; + if (object is LinearGradientBase) { + object.endY = value; + } break; case LinearGradientBase.opacityPropertyKey: - if (object is LinearGradientBase) object.opacity = value; + if (object is LinearGradientBase) { + object.opacity = value; + } break; case StrokeBase.thicknessPropertyKey: - if (object is StrokeBase) object.thickness = value; + if (object is StrokeBase) { + object.thickness = value; + } break; case GradientStopBase.positionPropertyKey: - if (object is GradientStopBase) object.position = value; + if (object is GradientStopBase) { + object.position = value; + } break; case TrimPathBase.startPropertyKey: - if (object is TrimPathBase) object.start = value; + if (object is TrimPathBase) { + object.start = value; + } break; case TrimPathBase.endPropertyKey: - if (object is TrimPathBase) object.end = value; + if (object is TrimPathBase) { + object.end = value; + } break; case TrimPathBase.offsetPropertyKey: - if (object is TrimPathBase) object.offset = value; + if (object is TrimPathBase) { + object.offset = value; + } break; case TransformComponentBase.rotationPropertyKey: - if (object is TransformComponentBase) object.rotation = value; + if (object is TransformComponentBase) { + object.rotation = value; + } break; case TransformComponentBase.scaleXPropertyKey: - if (object is TransformComponentBase) object.scaleX = value; + if (object is TransformComponentBase) { + object.scaleX = value; + } break; case TransformComponentBase.scaleYPropertyKey: - if (object is TransformComponentBase) object.scaleY = value; + if (object is TransformComponentBase) { + object.scaleY = value; + } break; case TransformComponentBase.opacityPropertyKey: - if (object is TransformComponentBase) object.opacity = value; + if (object is TransformComponentBase) { + object.opacity = value; + } break; case NodeBase.xPropertyKey: - if (object is NodeBase) object.x = value; + if (object is NodeBase) { + object.x = value; + } break; case NodeBase.yPropertyKey: - if (object is NodeBase) object.y = value; + if (object is NodeBase) { + object.y = value; + } break; case PathVertexBase.xPropertyKey: - if (object is PathVertexBase) object.x = value; + if (object is PathVertexBase) { + object.x = value; + } break; case PathVertexBase.yPropertyKey: - if (object is PathVertexBase) object.y = value; + if (object is PathVertexBase) { + object.y = value; + } break; case StraightVertexBase.radiusPropertyKey: - if (object is StraightVertexBase) object.radius = value; + if (object is StraightVertexBase) { + object.radius = value; + } break; case CubicAsymmetricVertexBase.rotationPropertyKey: - if (object is CubicAsymmetricVertexBase) object.rotation = value; + if (object is CubicAsymmetricVertexBase) { + object.rotation = value; + } break; case CubicAsymmetricVertexBase.inDistancePropertyKey: - if (object is CubicAsymmetricVertexBase) object.inDistance = value; + if (object is CubicAsymmetricVertexBase) { + object.inDistance = value; + } break; case CubicAsymmetricVertexBase.outDistancePropertyKey: - if (object is CubicAsymmetricVertexBase) object.outDistance = value; + if (object is CubicAsymmetricVertexBase) { + object.outDistance = value; + } break; case ParametricPathBase.widthPropertyKey: - if (object is ParametricPathBase) object.width = value; + if (object is ParametricPathBase) { + object.width = value; + } break; case ParametricPathBase.heightPropertyKey: - if (object is ParametricPathBase) object.height = value; + if (object is ParametricPathBase) { + object.height = value; + } break; case ParametricPathBase.originXPropertyKey: - if (object is ParametricPathBase) object.originX = value; + if (object is ParametricPathBase) { + object.originX = value; + } break; case ParametricPathBase.originYPropertyKey: - if (object is ParametricPathBase) object.originY = value; + if (object is ParametricPathBase) { + object.originY = value; + } break; case RectangleBase.cornerRadiusTLPropertyKey: - if (object is RectangleBase) object.cornerRadiusTL = value; + if (object is RectangleBase) { + object.cornerRadiusTL = value; + } break; case RectangleBase.cornerRadiusTRPropertyKey: - if (object is RectangleBase) object.cornerRadiusTR = value; + if (object is RectangleBase) { + object.cornerRadiusTR = value; + } break; case RectangleBase.cornerRadiusBLPropertyKey: - if (object is RectangleBase) object.cornerRadiusBL = value; + if (object is RectangleBase) { + object.cornerRadiusBL = value; + } break; case RectangleBase.cornerRadiusBRPropertyKey: - if (object is RectangleBase) object.cornerRadiusBR = value; + if (object is RectangleBase) { + object.cornerRadiusBR = value; + } break; case CubicMirroredVertexBase.rotationPropertyKey: - if (object is CubicMirroredVertexBase) object.rotation = value; + if (object is CubicMirroredVertexBase) { + object.rotation = value; + } break; case CubicMirroredVertexBase.distancePropertyKey: - if (object is CubicMirroredVertexBase) object.distance = value; + if (object is CubicMirroredVertexBase) { + object.distance = value; + } break; case PolygonBase.cornerRadiusPropertyKey: - if (object is PolygonBase) object.cornerRadius = value; + if (object is PolygonBase) { + object.cornerRadius = value; + } break; case StarBase.innerRadiusPropertyKey: - if (object is StarBase) object.innerRadius = value; + if (object is StarBase) { + object.innerRadius = value; + } break; case CubicDetachedVertexBase.inRotationPropertyKey: - if (object is CubicDetachedVertexBase) object.inRotation = value; + if (object is CubicDetachedVertexBase) { + object.inRotation = value; + } break; case CubicDetachedVertexBase.inDistancePropertyKey: - if (object is CubicDetachedVertexBase) object.inDistance = value; + if (object is CubicDetachedVertexBase) { + object.inDistance = value; + } break; case CubicDetachedVertexBase.outRotationPropertyKey: - if (object is CubicDetachedVertexBase) object.outRotation = value; + if (object is CubicDetachedVertexBase) { + object.outRotation = value; + } break; case CubicDetachedVertexBase.outDistancePropertyKey: - if (object is CubicDetachedVertexBase) object.outDistance = value; + if (object is CubicDetachedVertexBase) { + object.outDistance = value; + } break; case ArtboardBase.widthPropertyKey: - if (object is ArtboardBase) object.width = value; + if (object is ArtboardBase) { + object.width = value; + } break; case ArtboardBase.heightPropertyKey: - if (object is ArtboardBase) object.height = value; + if (object is ArtboardBase) { + object.height = value; + } break; case ArtboardBase.xPropertyKey: - if (object is ArtboardBase) object.x = value; + if (object is ArtboardBase) { + object.x = value; + } break; case ArtboardBase.yPropertyKey: - if (object is ArtboardBase) object.y = value; + if (object is ArtboardBase) { + object.y = value; + } break; case ArtboardBase.originXPropertyKey: - if (object is ArtboardBase) object.originX = value; + if (object is ArtboardBase) { + object.originX = value; + } break; case ArtboardBase.originYPropertyKey: - if (object is ArtboardBase) object.originY = value; + if (object is ArtboardBase) { + object.originY = value; + } break; case BoneBase.lengthPropertyKey: - if (object is BoneBase) object.length = value; + if (object is BoneBase) { + object.length = value; + } break; case RootBoneBase.xPropertyKey: - if (object is RootBoneBase) object.x = value; + if (object is RootBoneBase) { + object.x = value; + } break; case RootBoneBase.yPropertyKey: - if (object is RootBoneBase) object.y = value; + if (object is RootBoneBase) { + object.y = value; + } break; case SkinBase.xxPropertyKey: - if (object is SkinBase) object.xx = value; + if (object is SkinBase) { + object.xx = value; + } break; case SkinBase.yxPropertyKey: - if (object is SkinBase) object.yx = value; + if (object is SkinBase) { + object.yx = value; + } break; case SkinBase.xyPropertyKey: - if (object is SkinBase) object.xy = value; + if (object is SkinBase) { + object.xy = value; + } break; case SkinBase.yyPropertyKey: - if (object is SkinBase) object.yy = value; + if (object is SkinBase) { + object.yy = value; + } break; case SkinBase.txPropertyKey: - if (object is SkinBase) object.tx = value; + if (object is SkinBase) { + object.tx = value; + } break; case SkinBase.tyPropertyKey: - if (object is SkinBase) object.ty = value; + if (object is SkinBase) { + object.ty = value; + } break; case TendonBase.xxPropertyKey: - if (object is TendonBase) object.xx = value; + if (object is TendonBase) { + object.xx = value; + } break; case TendonBase.yxPropertyKey: - if (object is TendonBase) object.yx = value; + if (object is TendonBase) { + object.yx = value; + } break; case TendonBase.xyPropertyKey: - if (object is TendonBase) object.xy = value; + if (object is TendonBase) { + object.xy = value; + } break; case TendonBase.yyPropertyKey: - if (object is TendonBase) object.yy = value; + if (object is TendonBase) { + object.yy = value; + } break; case TendonBase.txPropertyKey: - if (object is TendonBase) object.tx = value; + if (object is TendonBase) { + object.tx = value; + } break; case TendonBase.tyPropertyKey: - if (object is TendonBase) object.ty = value; + if (object is TendonBase) { + object.ty = value; + } break; } } @@ -1579,13 +1883,19 @@ class RiveCoreContext { static void setColor(Core object, int propertyKey, int value) { switch (propertyKey) { case KeyFrameColorBase.valuePropertyKey: - if (object is KeyFrameColorBase) object.value = value; + if (object is KeyFrameColorBase) { + object.value = value; + } break; case SolidColorBase.colorValuePropertyKey: - if (object is SolidColorBase) object.colorValue = value; + if (object is SolidColorBase) { + object.colorValue = value; + } break; case GradientStopBase.colorValuePropertyKey: - if (object is GradientStopBase) object.colorValue = value; + if (object is GradientStopBase) { + object.colorValue = value; + } break; } } @@ -1593,25 +1903,39 @@ class RiveCoreContext { static void setBool(Core object, int propertyKey, bool value) { switch (propertyKey) { case LinearAnimationBase.enableWorkAreaPropertyKey: - if (object is LinearAnimationBase) object.enableWorkArea = value; + if (object is LinearAnimationBase) { + object.enableWorkArea = value; + } break; case StateMachineBoolBase.valuePropertyKey: - if (object is StateMachineBoolBase) object.value = value; + if (object is StateMachineBoolBase) { + object.value = value; + } break; case ShapePaintBase.isVisiblePropertyKey: - if (object is ShapePaintBase) object.isVisible = value; + if (object is ShapePaintBase) { + object.isVisible = value; + } break; case StrokeBase.transformAffectsStrokePropertyKey: - if (object is StrokeBase) object.transformAffectsStroke = value; + if (object is StrokeBase) { + object.transformAffectsStroke = value; + } break; case PointsPathBase.isClosedPropertyKey: - if (object is PointsPathBase) object.isClosed = value; + if (object is PointsPathBase) { + object.isClosed = value; + } break; case RectangleBase.linkCornerRadiusPropertyKey: - if (object is RectangleBase) object.linkCornerRadius = value; + if (object is RectangleBase) { + object.linkCornerRadius = value; + } break; case ClippingShapeBase.isVisiblePropertyKey: - if (object is ClippingShapeBase) object.isVisible = value; + if (object is ClippingShapeBase) { + object.isVisible = value; + } break; } } diff --git a/lib/src/rive_core/animation/animation.dart b/lib/src/rive_core/animation/animation.dart index a1afb27..0fabb31 100644 --- a/lib/src/rive_core/animation/animation.dart +++ b/lib/src/rive_core/animation/animation.dart @@ -1,5 +1,6 @@ import 'package:rive/src/core/core.dart'; import 'package:rive/src/rive_core/artboard.dart'; + import 'package:rive/src/generated/animation/animation_base.dart'; export 'package:rive/src/generated/animation/animation_base.dart'; @@ -17,10 +18,13 @@ class Animation extends AnimationBase { @override void onAddedDirty() {} + @override void onAdded() {} + @override bool validate() => super.validate() && _artboard != null; + @override void nameChanged(String from, String to) {} } diff --git a/lib/src/rive_core/animation/animation_state.dart b/lib/src/rive_core/animation/animation_state.dart index 5cb2c3f..f0805fa 100644 --- a/lib/src/rive_core/animation/animation_state.dart +++ b/lib/src/rive_core/animation/animation_state.dart @@ -1,5 +1,8 @@ import 'package:rive/src/core/core.dart'; +import 'package:rive/src/rive_core/animation/animation_state_instance.dart'; import 'package:rive/src/rive_core/animation/linear_animation.dart'; +import 'package:rive/src/rive_core/animation/state_instance.dart'; +import 'package:rive/src/rive_core/artboard.dart'; import 'package:rive/src/generated/animation/animation_state_base.dart'; export 'package:rive/src/generated/animation/animation_state_base.dart'; @@ -15,6 +18,7 @@ class AnimationState extends AnimationStateBase { if (_animation == value) { return; } + _animation = value; animationId = value?.id ?? Core.missingId; } @@ -23,4 +27,33 @@ class AnimationState extends AnimationStateBase { void animationIdChanged(int from, int to) { animation = id == Core.missingId ? null : context.resolve(to); } + + @override + StateInstance makeInstance() { + if (animation == null) { + // Failed to load at runtime/some new type we don't understand. + return SystemStateInstance(this); + } + + return AnimationStateInstance(this); + } + + // We keep the importer code here so that we can inject this for runtime. + // #2690 + @override + bool import(ImportStack stack) { + var importer = stack.latest(ArtboardBase.typeKey); + if (importer == null) { + return false; + } + + if (animationId >= 0 && animationId < importer.artboard.animations.length) { + var found = importer.artboard.animations[animationId]; + if (found is LinearAnimation) { + animation = found; + } + } + + return super.import(stack); + } } diff --git a/lib/src/rive_core/animation/animation_state_instance.dart b/lib/src/rive_core/animation/animation_state_instance.dart new file mode 100644 index 0000000..906fe0b --- /dev/null +++ b/lib/src/rive_core/animation/animation_state_instance.dart @@ -0,0 +1,25 @@ +import 'package:rive/src/core/core.dart'; +import 'package:rive/src/rive_core/animation/animation_state.dart'; +import 'package:rive/src/rive_core/animation/linear_animation_instance.dart'; +import 'package:rive/src/rive_core/animation/state_instance.dart'; + +/// Simple wrapper around [LinearAnimationInstance] making it compatible with +/// the [StateMachine]'s [StateInstance] interface. +class AnimationStateInstance extends StateInstance { + final LinearAnimationInstance animationInstance; + + AnimationStateInstance(AnimationState state) + : assert(state.animation != null), + animationInstance = LinearAnimationInstance(state.animation!), + super(state); + + @override + void advance(double seconds, _) => animationInstance.advance(seconds); + + @override + void apply(CoreContext core, double mix) => animationInstance.animation + .apply(animationInstance.time, coreContext: core, mix: mix); + + @override + bool get keepGoing => animationInstance.keepGoing; +} diff --git a/lib/src/rive_core/animation/any_state.dart b/lib/src/rive_core/animation/any_state.dart index 116405f..d1d2376 100644 --- a/lib/src/rive_core/animation/any_state.dart +++ b/lib/src/rive_core/animation/any_state.dart @@ -1,4 +1,8 @@ +import 'package:rive/src/rive_core/animation/state_instance.dart'; import 'package:rive/src/generated/animation/any_state_base.dart'; export 'package:rive/src/generated/animation/any_state_base.dart'; -class AnyState extends AnyStateBase {} +class AnyState extends AnyStateBase { + @override + StateInstance makeInstance() => SystemStateInstance(this); +} diff --git a/lib/src/rive_core/animation/blend_animation.dart b/lib/src/rive_core/animation/blend_animation.dart new file mode 100644 index 0000000..3b0fee1 --- /dev/null +++ b/lib/src/rive_core/animation/blend_animation.dart @@ -0,0 +1,52 @@ +import 'package:rive/src/core/core.dart'; +import 'package:rive/src/rive_core/animation/layer_state.dart'; +import 'package:rive/src/rive_core/animation/linear_animation.dart'; +import 'package:rive/src/rive_core/artboard.dart'; +import 'package:rive/src/generated/animation/blend_animation_base.dart'; +import 'package:rive/src/generated/artboard_base.dart'; +export 'package:rive/src/generated/animation/blend_animation_base.dart'; + +abstract class BlendAnimation extends BlendAnimationBase { + LinearAnimation? _animation; + LinearAnimation? get animation => _animation; + + @override + void animationIdChanged(int from, int to) { + _animation = context.resolve(to); + } + + @override + void onAdded() {} + + @override + void onRemoved() { + super.onRemoved(); + } + + @override + void onAddedDirty() {} + + @override + bool import(ImportStack importStack) { + var importer = + importStack.latest(LayerStateBase.typeKey); + if (importer == null || !importer.addBlendAnimation(this)) { + return false; + } + var artboardImporter = + importStack.latest(ArtboardBase.typeKey); + if (artboardImporter == null) { + return false; + } + + if (animationId >= 0 && + animationId < artboardImporter.artboard.animations.length) { + var found = artboardImporter.artboard.animations[animationId]; + if (found is LinearAnimation) { + _animation = found; + } + } + + return super.import(importStack); + } +} diff --git a/lib/src/rive_core/animation/blend_animation_1d.dart b/lib/src/rive_core/animation/blend_animation_1d.dart new file mode 100644 index 0000000..99d8a52 --- /dev/null +++ b/lib/src/rive_core/animation/blend_animation_1d.dart @@ -0,0 +1,9 @@ +import 'package:rive/src/generated/animation/blend_animation_1d_base.dart'; +export 'package:rive/src/generated/animation/blend_animation_1d_base.dart'; + +class BlendAnimation1D extends BlendAnimation1DBase { + @override + void valueChanged(double from, double to) { + // TODO: implement valueChanged + } +} diff --git a/lib/src/rive_core/animation/blend_animation_direct.dart b/lib/src/rive_core/animation/blend_animation_direct.dart new file mode 100644 index 0000000..8fe9e8e --- /dev/null +++ b/lib/src/rive_core/animation/blend_animation_direct.dart @@ -0,0 +1,30 @@ +import 'package:rive/src/core/core.dart'; +import 'package:rive/src/rive_core/animation/state_machine.dart'; +import 'package:rive/src/rive_core/animation/state_machine_number.dart'; +import 'package:rive/src/generated/animation/blend_animation_direct_base.dart'; +export 'package:rive/src/generated/animation/blend_animation_direct_base.dart'; + +class BlendAnimationDirect extends BlendAnimationDirectBase { + StateMachineNumber? _input; + StateMachineNumber? get input => _input; + + @override + void inputIdChanged(int from, int to) {} + + @override + bool import(ImportStack stack) { + var importer = stack.latest(StateMachineBase.typeKey); + if (importer == null) { + return false; + } + if (inputId >= 0 && inputId < importer.machine.inputs.length) { + var found = importer.machine.inputs[inputId]; + if (found is StateMachineNumber) { + _input = found; + inputId = found.id; + } + } + + return super.import(stack); + } +} diff --git a/lib/src/rive_core/animation/blend_state.dart b/lib/src/rive_core/animation/blend_state.dart new file mode 100644 index 0000000..e6a157b --- /dev/null +++ b/lib/src/rive_core/animation/blend_state.dart @@ -0,0 +1,20 @@ +import 'package:rive/src/core/core.dart'; +import 'package:rive/src/rive_core/animation/blend_animation.dart'; +import 'package:rive/src/generated/animation/blend_state_base.dart'; +export 'package:rive/src/generated/animation/blend_state_base.dart'; + +// +abstract class BlendState extends BlendStateBase { + final BlendAnimations _animations = BlendAnimations(); + BlendAnimations get animations => _animations; + + void internalAddAnimation(T animation) { + assert(!_animations.contains(animation), + 'shouln\'t already contain the animation'); + _animations.add(animation); + } + + void internalRemoveAnimation(T animation) { + _animations.remove(animation); + } +} diff --git a/lib/src/rive_core/animation/blend_state_1d.dart b/lib/src/rive_core/animation/blend_state_1d.dart new file mode 100644 index 0000000..dd8859d --- /dev/null +++ b/lib/src/rive_core/animation/blend_state_1d.dart @@ -0,0 +1,24 @@ +import 'package:rive/src/rive_core/animation/blend_state_1d_instance.dart'; +import 'package:rive/src/rive_core/animation/state_instance.dart'; +import 'package:rive/src/rive_core/animation/state_machine_number.dart'; +import 'package:rive/src/generated/animation/blend_state_1d_base.dart'; +export 'package:rive/src/generated/animation/blend_state_1d_base.dart'; + +class BlendState1D extends BlendState1DBase { + StateMachineNumber? _input; + StateMachineNumber? get input => _input; + + @override + void inputIdChanged(int from, int to) { + _input = context.resolve(to); + } + + @override + void onAddedDirty() { + super.onAddedDirty(); + _input = context.resolve(inputId); + } + + @override + StateInstance makeInstance() => BlendState1DInstance(this); +} diff --git a/lib/src/rive_core/animation/blend_state_1d_instance.dart b/lib/src/rive_core/animation/blend_state_1d_instance.dart new file mode 100644 index 0000000..29dd85b --- /dev/null +++ b/lib/src/rive_core/animation/blend_state_1d_instance.dart @@ -0,0 +1,82 @@ +import 'dart:collection'; + +import 'package:rive/src/rive_core/animation/blend_animation_1d.dart'; +import 'package:rive/src/rive_core/animation/blend_state_1d.dart'; +import 'package:rive/src/rive_core/animation/blend_state_instance.dart'; + +/// [BlendState1D] mixing logic that runs inside the [StateMachine]. +class BlendState1DInstance + extends BlendStateInstance { + BlendState1DInstance(BlendState1D state) : super(state) { + animationInstances.sort( + (a, b) => a.blendAnimation.value.compareTo(b.blendAnimation.value)); + } + + /// Binary find the closest animation index. + int animationIndex(double value) { + int idx = 0; + int mid = 0; + double closestValue = 0; + int start = 0; + int end = animationInstances.length - 1; + + while (start <= end) { + mid = (start + end) >> 1; + closestValue = animationInstances[mid].blendAnimation.value; + if (closestValue < value) { + start = mid + 1; + } else if (closestValue > value) { + end = mid - 1; + } else { + idx = start = mid; + break; + } + + idx = start; + } + return idx; + } + + BlendStateAnimationInstance? _from; + BlendStateAnimationInstance? _to; + + @override + void advance(double seconds, HashMap inputValues) { + super.advance(seconds, inputValues); + dynamic inputValue = inputValues[(state as BlendState1D).inputId]; + var value = (inputValue is double + ? inputValue + : (state as BlendState1D).input?.value) ?? + 0; + int index = animationIndex(value); + _to = index >= 0 && index < animationInstances.length + ? animationInstances[index] + : null; + _from = index - 1 >= 0 && index - 1 < animationInstances.length + ? animationInstances[index - 1] + : null; + + double mix, mixFrom; + if (_to == null || + _from == null || + _to!.blendAnimation.value == _from!.blendAnimation.value) { + mix = mixFrom = 1; + } else { + mix = (value - _from!.blendAnimation.value) / + (_to!.blendAnimation.value - _from!.blendAnimation.value); + mixFrom = 1.0 - mix; + } + + var toValue = _to?.blendAnimation.value; + var fromValue = _from?.blendAnimation.value; + for (final animation in animationInstances) { + if (animation.blendAnimation.value == toValue) { + animation.mix = mix; + } else if (animation.blendAnimation.value == fromValue) { + animation.mix = mixFrom; + } else { + animation.mix = 0; + } + } + } +} diff --git a/lib/src/rive_core/animation/blend_state_direct.dart b/lib/src/rive_core/animation/blend_state_direct.dart new file mode 100644 index 0000000..9f62c40 --- /dev/null +++ b/lib/src/rive_core/animation/blend_state_direct.dart @@ -0,0 +1,9 @@ +import 'package:rive/src/rive_core/animation/blend_state_direct_instance.dart'; +import 'package:rive/src/rive_core/animation/state_instance.dart'; +import 'package:rive/src/generated/animation/blend_state_direct_base.dart'; +export 'package:rive/src/generated/animation/blend_state_direct_base.dart'; + +class BlendStateDirect extends BlendStateDirectBase { + @override + StateInstance makeInstance() => BlendStateDirectInstance(this); +} diff --git a/lib/src/rive_core/animation/blend_state_direct_instance.dart b/lib/src/rive_core/animation/blend_state_direct_instance.dart new file mode 100644 index 0000000..dde8470 --- /dev/null +++ b/lib/src/rive_core/animation/blend_state_direct_instance.dart @@ -0,0 +1,24 @@ +import 'dart:collection'; + +import 'package:rive/src/rive_core/animation/blend_animation_direct.dart'; +import 'package:rive/src/rive_core/animation/blend_state_direct.dart'; +import 'package:rive/src/rive_core/animation/blend_state_instance.dart'; + +/// [BlendStateDirect] mixing logic that runs inside the [StateMachine]. +class BlendStateDirectInstance + extends BlendStateInstance { + BlendStateDirectInstance(BlendStateDirect state) : super(state); + + @override + void advance(double seconds, HashMap inputValues) { + super.advance(seconds, inputValues); + for (final animation in animationInstances) { + dynamic inputValue = inputValues[animation.blendAnimation.inputId]; + var value = (inputValue is double + ? inputValue + : animation.blendAnimation.input?.value) ?? + 0; + animation.mix = value / 100; + } + } +} diff --git a/lib/src/rive_core/animation/blend_state_instance.dart b/lib/src/rive_core/animation/blend_state_instance.dart new file mode 100644 index 0000000..6076f16 --- /dev/null +++ b/lib/src/rive_core/animation/blend_state_instance.dart @@ -0,0 +1,59 @@ +import 'dart:collection'; + +import 'package:rive/src/core/core.dart'; +import 'package:flutter/foundation.dart'; +import 'package:rive/src/rive_core/animation/blend_animation.dart'; +import 'package:rive/src/rive_core/animation/blend_state.dart'; +import 'package:rive/src/rive_core/animation/linear_animation_instance.dart'; +import 'package:rive/src/rive_core/animation/state_instance.dart'; + +/// Individual animation in a blend state instance. +class BlendStateAnimationInstance { + final T blendAnimation; + final LinearAnimationInstance animationInstance; + double mix = 0; + + BlendStateAnimationInstance(this.blendAnimation) + : animationInstance = LinearAnimationInstance(blendAnimation.animation!); +} + +/// Generic blend state instance which works for [BlendState]s +/// where T represents the BlendState and K the BlendAnimation. +abstract class BlendStateInstance, + K extends BlendAnimation> extends StateInstance { + final List> animationInstances; + BlendStateInstance(T state) + : animationInstances = state.animations + .where((animation) => animation.animation != null) + .map((animation) => BlendStateAnimationInstance(animation)) + .toList(growable: false), + super(state); + + bool _keepGoing = true; + @override + bool get keepGoing => _keepGoing; + + @mustCallSuper + @override + void advance(double seconds, HashMap inputValues) { + _keepGoing = false; + // Advance all the animations in the blend state + for (final animation in animationInstances) { + if (animation.animationInstance.advance(seconds) && !keepGoing) { + _keepGoing = true; + } + } + } + + @override + void apply(CoreContext core, double mix) { + for (final animation in animationInstances) { + double m = mix * animation.mix; + if (m == 0) { + continue; + } + animation.animationInstance.animation + .apply(animation.animationInstance.time, coreContext: core, mix: m); + } + } +} diff --git a/lib/src/rive_core/animation/blend_state_transition.dart b/lib/src/rive_core/animation/blend_state_transition.dart new file mode 100644 index 0000000..6b76d5c --- /dev/null +++ b/lib/src/rive_core/animation/blend_state_transition.dart @@ -0,0 +1,31 @@ +import 'package:rive/src/rive_core/animation/blend_animation.dart'; +import 'package:rive/src/rive_core/animation/blend_state_instance.dart'; +import 'package:rive/src/rive_core/animation/layer_state.dart'; +import 'package:rive/src/rive_core/animation/linear_animation.dart'; +import 'package:rive/src/rive_core/animation/linear_animation_instance.dart'; +import 'package:rive/src/rive_core/animation/state_instance.dart'; +import 'package:rive/src/generated/animation/blend_state_transition_base.dart'; +export 'package:rive/src/generated/animation/blend_state_transition_base.dart'; + +class BlendStateTransition extends BlendStateTransitionBase { + BlendAnimation? exitBlendAnimation; + + @override + LinearAnimationInstance? exitTimeAnimationInstance(StateInstance stateFrom) { + if (stateFrom is BlendStateInstance) { + for (final blendAnimation in stateFrom.animationInstances) { + if (blendAnimation.blendAnimation == exitBlendAnimation) { + return blendAnimation.animationInstance; + } + } + } + return null; + } + + @override + LinearAnimation? exitTimeAnimation(LayerState stateFrom) => + exitBlendAnimation?.animation; + + @override + void exitBlendAnimationIdChanged(int from, int to) {} +} diff --git a/lib/src/rive_core/animation/cubic_interpolator.dart b/lib/src/rive_core/animation/cubic_interpolator.dart index c972192..8216745 100644 --- a/lib/src/rive_core/animation/cubic_interpolator.dart +++ b/lib/src/rive_core/animation/cubic_interpolator.dart @@ -1,14 +1,18 @@ import 'dart:typed_data'; + import 'package:rive/src/core/core.dart'; import 'package:rive/src/rive_core/animation/interpolator.dart'; import 'package:rive/src/rive_core/artboard.dart'; import 'package:rive/src/generated/animation/cubic_interpolator_base.dart'; const int newtonIterations = 4; + +// Implements https://github.com/gre/bezier-easing/blob/master/src/index.js const double newtonMinSlope = 0.001; const double sampleStepSize = 1.0 / (splineTableSize - 1.0); const int splineTableSize = 11; const int subdivisionMaxIterations = 10; + const double subdivisionPrecision = 0.0000001; double _calcBezier(double aT, double aA1, double aA2) { return (((1.0 - 3.0 * aA2 + 3.0 * aA1) * aT + (3.0 * aA2 - 6.0 * aA1)) * aT + @@ -16,14 +20,17 @@ double _calcBezier(double aT, double aA1, double aA2) { aT; } +// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. double _getSlope(double aT, double aA1, double aA2) { return 3.0 * (1.0 - 3.0 * aA2 + 3.0 * aA1) * aT * aT + 2.0 * (3.0 * aA2 - 6.0 * aA1) * aT + (3.0 * aA1); } +// Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. class CubicInterpolator extends CubicInterpolatorBase implements Interpolator { _CubicEase _ease = _CubicEase.make(0.42, 0, 0.58, 1); + @override bool equalParameters(Interpolator other) { if (other is CubicInterpolator) { @@ -37,16 +44,22 @@ class CubicInterpolator extends CubicInterpolatorBase implements Interpolator { @override void onAdded() => _updateStoredCubic(); + @override void onAddedDirty() {} + @override double transform(double value) => _ease.transform(value); + @override void x1Changed(double from, double to) => _updateStoredCubic(); + @override void x2Changed(double from, double to) => _updateStoredCubic(); + @override void y1Changed(double from, double to) => _updateStoredCubic(); + @override void y2Changed(double from, double to) => _updateStoredCubic(); void _updateStoredCubic() { @@ -60,6 +73,7 @@ class CubicInterpolator extends CubicInterpolatorBase implements Interpolator { return false; } artboardHelper.addComponent(this); + return super.import(stack); } } @@ -68,23 +82,29 @@ class _Cubic extends _CubicEase { final Float64List _values = Float64List(splineTableSize); final double x1, y1, x2, y2; _Cubic(this.x1, this.y1, this.x2, this.y2) { + // Precompute values table for (int i = 0; i < splineTableSize; ++i) { _values[i] = _calcBezier(i * sampleStepSize, x1, x2); } } + double getT(double x) { double intervalStart = 0.0; int currentSample = 1; int lastSample = splineTableSize - 1; + for (; currentSample != lastSample && _values[currentSample] <= x; ++currentSample) { intervalStart += sampleStepSize; } --currentSample; + + // Interpolate to provide an initial guess for t var dist = (x - _values[currentSample]) / (_values[currentSample + 1] - _values[currentSample]); var guessForT = intervalStart + dist * sampleStepSize; + var initialSlope = _getSlope(guessForT, x1, x2); if (initialSlope >= newtonMinSlope) { for (int i = 0; i < newtonIterations; ++i) { @@ -124,6 +144,7 @@ class _Cubic extends _CubicEase { abstract class _CubicEase { double transform(double t); + static _CubicEase make(double x1, double y1, double x2, double y2) { if (x1 == y1 && x2 == y2) { return _LinearCubicEase(); diff --git a/lib/src/rive_core/animation/entry_state.dart b/lib/src/rive_core/animation/entry_state.dart index 81f9c97..d868ef3 100644 --- a/lib/src/rive_core/animation/entry_state.dart +++ b/lib/src/rive_core/animation/entry_state.dart @@ -1,4 +1,8 @@ +import 'package:rive/src/rive_core/animation/state_instance.dart'; import 'package:rive/src/generated/animation/entry_state_base.dart'; export 'package:rive/src/generated/animation/entry_state_base.dart'; -class EntryState extends EntryStateBase {} +class EntryState extends EntryStateBase { + @override + StateInstance makeInstance() => SystemStateInstance(this); +} diff --git a/lib/src/rive_core/animation/exit_state.dart b/lib/src/rive_core/animation/exit_state.dart index a8e6e1c..fabdec2 100644 --- a/lib/src/rive_core/animation/exit_state.dart +++ b/lib/src/rive_core/animation/exit_state.dart @@ -1,4 +1,8 @@ +import 'package:rive/src/rive_core/animation/state_instance.dart'; import 'package:rive/src/generated/animation/exit_state_base.dart'; export 'package:rive/src/generated/animation/exit_state_base.dart'; -class ExitState extends ExitStateBase {} +class ExitState extends ExitStateBase { + @override + StateInstance makeInstance() => SystemStateInstance(this); +} diff --git a/lib/src/rive_core/animation/interpolator.dart b/lib/src/rive_core/animation/interpolator.dart index 88d4e4c..8526b5b 100644 --- a/lib/src/rive_core/animation/interpolator.dart +++ b/lib/src/rive_core/animation/interpolator.dart @@ -1,5 +1,8 @@ abstract class Interpolator { int get id; + + /// Convert a linear interpolation factor to an eased one. double transform(double value); + bool equalParameters(Interpolator other); } diff --git a/lib/src/rive_core/animation/keyed_object.dart b/lib/src/rive_core/animation/keyed_object.dart index d752b70..5778cf7 100644 --- a/lib/src/rive_core/animation/keyed_object.dart +++ b/lib/src/rive_core/animation/keyed_object.dart @@ -1,7 +1,9 @@ import 'dart:collection'; + import 'package:rive/src/core/core.dart'; import 'package:rive/src/rive_core/animation/keyed_property.dart'; import 'package:rive/src/rive_core/component.dart'; + import 'package:rive/src/generated/animation/keyed_object_base.dart'; import 'linear_animation.dart'; export 'package:rive/src/generated/animation/keyed_object_base.dart'; @@ -9,20 +11,26 @@ export 'package:rive/src/generated/animation/keyed_object_base.dart'; class KeyedObject extends KeyedObjectBase { final HashMap _keyedProperties = HashMap(); + Iterable get keyedProperties => _keyedProperties.values; + @override void onAddedDirty() {} + @override void onAdded() {} + @override bool validate() { if (!super.validate()) { return false; } + var component = context.resolve(objectId); if (component == null) { return false; } + return true; } @@ -33,14 +41,22 @@ class KeyedObject extends KeyedObjectBase { bool isValidKeyedProperty(KeyedProperty property) { var value = _keyedProperties[property.propertyKey]; + + // If the property is already keyed, that's ok just make sure the + // KeyedObject matches. if (value != null && value != property) { return false; } return true; } + /// Called by rive_core to add a KeyedProperty to the animation. This should + /// be @internal when it's supported. bool internalAddKeyedProperty(KeyedProperty property) { var value = _keyedProperties[property.propertyKey]; + + // If the property is already keyed, that's ok just make sure the + // KeyedObject matches. if (value != null && value != property) { return false; } @@ -48,11 +64,17 @@ class KeyedObject extends KeyedObjectBase { return true; } + /// Called by rive_core to remove a KeyedObject to the animation. This should + /// be @internal when it's supported. bool internalRemoveKeyedProperty(KeyedProperty property) { var removed = _keyedProperties.remove(property.propertyKey); + if (_keyedProperties.isEmpty) { + // Remove this keyed property. context.removeObject(this); } + // assert(removed == null || removed == property, + // '$removed was not $property or null'); return removed != null; } @@ -68,6 +90,7 @@ class KeyedObject extends KeyedObjectBase { @override void objectIdChanged(int from, int to) {} + @override bool import(ImportStack stack) { var animationHelper = @@ -76,6 +99,7 @@ class KeyedObject extends KeyedObjectBase { return false; } animationHelper.addKeyedObject(this); + return super.import(stack); } } diff --git a/lib/src/rive_core/animation/keyed_property.dart b/lib/src/rive_core/animation/keyed_property.dart index 5a9cec8..13e3f07 100644 --- a/lib/src/rive_core/animation/keyed_property.dart +++ b/lib/src/rive_core/animation/keyed_property.dart @@ -1,6 +1,7 @@ import 'package:rive/src/core/core.dart'; import 'package:rive/src/rive_core/animation/keyed_object.dart'; import 'package:rive/src/rive_core/animation/keyframe.dart'; + import 'package:rive/src/generated/animation/keyed_property_base.dart'; export 'package:rive/src/generated/animation/keyed_property_base.dart'; @@ -12,6 +13,8 @@ class KeyFrameList { List _keyframes = []; Iterable get keyframes => _keyframes; set keyframes(Iterable frames) => _keyframes = frames.toList(); + + /// Get the keyframe immediately following the provided one. T? after(T keyframe) { var index = _keyframes.indexOf(keyframe); if (index != -1 && index + 1 < _keyframes.length) { @@ -20,12 +23,15 @@ class KeyFrameList { return null; } + /// Find the index in the keyframe list of a specific time frame. int indexOfFrame(int frame) { int idx = 0; + // Binary find the keyframe index. int mid = 0; int closestFrame = 0; int start = 0; int end = _keyframes.length - 1; + while (start <= end) { mid = (start + end) >> 1; closestFrame = _keyframes[mid].frame; @@ -37,6 +43,7 @@ class KeyFrameList { idx = start = mid; break; } + idx = start; } return idx; @@ -49,13 +56,17 @@ class KeyedProperty extends KeyedPropertyBase with KeyFrameList { @override void onAdded() {} + @override void onAddedDirty() {} + @override void onRemoved() { super.onRemoved(); } + /// Called by rive_core to add a KeyFrame to this KeyedProperty. This should + /// be @internal when it's supported. bool internalAddKeyFrame(KeyFrame frame) { if (_keyframes.contains(frame)) { return false; @@ -65,44 +76,64 @@ class KeyedProperty extends KeyedPropertyBase return true; } + /// Called by rive_core to remove a KeyFrame from this KeyedProperty. This + /// should be @internal when it's supported. bool internalRemoveKeyFrame(KeyFrame frame) { var removed = _keyframes.remove(frame); if (_keyframes.isEmpty) { + // If they keyframes are now empty, we might want to remove this keyed + // property. Wait for any other pending changes to complete before + // checking. context.dirty(_checkShouldRemove); } + return removed; } void _checkShouldRemove() { if (_keyframes.isEmpty) { + // Remove this keyed property. context.removeObject(this); } } + /// Called by keyframes when their time value changes. This is a pretty rare + /// operation, usually occurs when a user moves a keyframe. Meaning: this + /// shouldn't make it into the runtimes unless we want to allow users moving + /// keyframes around at runtime via code for some reason. void markKeyFrameOrderDirty() { context.dirty(_sortAndValidateKeyFrames); } void _sortAndValidateKeyFrames() { sort(); + for (int i = 0; i < _keyframes.length - 1; i++) { var a = _keyframes[i]; var b = _keyframes[i + 1]; if (a.frame == b.frame) { + // N.B. this removes it from the list too. context.removeObject(a); + // Repeat current. i--; } } } + /// Number of keyframes for this keyed property. int get numFrames => _keyframes.length; + KeyFrame getFrameAt(int index) => _keyframes[index]; + int closestFrameIndex(double seconds) { int idx = 0; + // Binary find the keyframe index (use timeInSeconds here as opposed to the + // finder above which operates in frames). int mid = 0; double closestSeconds = 0; int start = 0; int end = _keyframes.length - 1; + while (start <= end) { mid = (start + end) >> 1; closestSeconds = _keyframes[mid].seconds; @@ -123,10 +154,12 @@ class KeyedProperty extends KeyedPropertyBase if (_keyframes.isEmpty) { return; } + int idx = closestFrameIndex(seconds); int pk = propertyKey; if (idx == 0) { var first = _keyframes[0]; + first.apply(object, pk, mix); } else { if (idx < _keyframes.length) { @@ -135,6 +168,8 @@ class KeyedProperty extends KeyedPropertyBase if (seconds == toFrame.seconds) { toFrame.apply(object, pk, mix); } else { + /// Equivalent to fromFrame.interpolation == + /// KeyFrameInterpolation.hold. if (fromFrame.interpolationType == 0) { fromFrame.apply(object, pk, mix); } else { @@ -143,6 +178,7 @@ class KeyedProperty extends KeyedPropertyBase } } else { var last = _keyframes[idx - 1]; + last.apply(object, pk, mix); } } @@ -150,6 +186,7 @@ class KeyedProperty extends KeyedPropertyBase @override void propertyKeyChanged(int from, int to) {} + @override bool import(ImportStack stack) { var importer = stack.latest(KeyedObjectBase.typeKey); @@ -157,6 +194,7 @@ class KeyedProperty extends KeyedPropertyBase return false; } importer.addKeyedProperty(this); + return super.import(stack); } } diff --git a/lib/src/rive_core/animation/keyframe.dart b/lib/src/rive_core/animation/keyframe.dart index 49a1747..9d82a62 100644 --- a/lib/src/rive_core/animation/keyframe.dart +++ b/lib/src/rive_core/animation/keyframe.dart @@ -3,14 +3,18 @@ import 'package:rive/src/rive_core/animation/interpolator.dart'; import 'package:rive/src/rive_core/animation/keyed_property.dart'; import 'package:rive/src/rive_core/animation/keyframe_interpolation.dart'; import 'package:rive/src/rive_core/animation/linear_animation.dart'; + import 'package:rive/src/generated/animation/keyframe_base.dart'; + export 'package:rive/src/generated/animation/keyframe_base.dart'; abstract class KeyFrame extends KeyFrameBase implements KeyFrameInterface { double _timeInSeconds = 0; double get seconds => _timeInSeconds; + bool get canInterpolate => true; + KeyFrameInterpolation get interpolation => KeyFrameInterpolation.values[interpolationType]; set interpolation(KeyFrameInterpolation value) { @@ -19,13 +23,17 @@ abstract class KeyFrame extends KeyFrameBase @override void interpolationTypeChanged(int from, int to) {} + @override void interpolatorIdChanged(int from, int to) { + // This might resolve to null during a load or if context isn't available + // yet so we also do this in onAddedDirty. interpolator = context.resolve(to); } @override void onAdded() {} + void computeSeconds(LinearAnimation animation) { _timeInSeconds = frame / animation.fps; } @@ -44,15 +52,22 @@ abstract class KeyFrame extends KeyFrameBase @override void frameChanged(int from, int to) {} + + /// Apply the value of this keyframe to the object's property. void apply(Core object, int propertyKey, double mix); + + /// Interpolate the value between this keyframe and the next and apply it to + /// the object's property. void applyInterpolation(Core object, int propertyKey, double seconds, covariant KeyFrame nextFrame, double mix); + Interpolator? _interpolator; Interpolator? get interpolator => _interpolator; set interpolator(Interpolator? value) { if (_interpolator == value) { return; } + _interpolator = value; interpolatorId = value?.id ?? Core.missingId; } @@ -65,6 +80,7 @@ abstract class KeyFrame extends KeyFrameBase return false; } keyedPropertyHelper.addKeyFrame(this); + return super.import(importStack); } } diff --git a/lib/src/rive_core/animation/keyframe_color.dart b/lib/src/rive_core/animation/keyframe_color.dart index a62a0cd..94c8393 100644 --- a/lib/src/rive_core/animation/keyframe_color.dart +++ b/lib/src/rive_core/animation/keyframe_color.dart @@ -1,4 +1,5 @@ import 'dart:ui'; + import 'package:rive/src/core/core.dart'; import 'package:rive/src/generated/animation/keyframe_color_base.dart'; import 'package:rive/src/generated/rive_core_context.dart'; @@ -22,13 +23,16 @@ class KeyFrameColor extends KeyFrameColorBase { @override void apply(Core object, int propertyKey, double mix) => _apply(object, propertyKey, mix, value); + @override void applyInterpolation(Core object, int propertyKey, double currentTime, KeyFrameColor nextFrame, double mix) { var f = (currentTime - seconds) / (nextFrame.seconds - seconds); + if (interpolator != null) { f = interpolator!.transform(f); } + var color = Color.lerp(Color(value), Color(nextFrame.value), f); if (color != null) { _apply(object, propertyKey, mix, color.value); diff --git a/lib/src/rive_core/animation/keyframe_double.dart b/lib/src/rive_core/animation/keyframe_double.dart index 2d902f4..4dab3c9 100644 --- a/lib/src/rive_core/animation/keyframe_double.dart +++ b/lib/src/rive_core/animation/keyframe_double.dart @@ -22,13 +22,16 @@ class KeyFrameDouble extends KeyFrameDoubleBase { @override void apply(Core object, int propertyKey, double mix) => _apply(object, propertyKey, mix, value); + @override void applyInterpolation(Core object, int propertyKey, double currentTime, KeyFrameDouble nextFrame, double mix) { var f = (currentTime - seconds) / (nextFrame.seconds - seconds); + if (interpolator != null) { f = interpolator!.transform(f); } + _apply(object, propertyKey, mix, value + (nextFrame.value - value) * f); } diff --git a/lib/src/rive_core/animation/keyframe_id.dart b/lib/src/rive_core/animation/keyframe_id.dart index 235029c..fa6accb 100644 --- a/lib/src/rive_core/animation/keyframe_id.dart +++ b/lib/src/rive_core/animation/keyframe_id.dart @@ -6,6 +6,7 @@ export 'package:rive/src/generated/animation/keyframe_id_base.dart'; class KeyFrameId extends KeyFrameIdBase { @override bool get canInterpolate => false; + @override void apply(Core object, int propertyKey, double mix) { RiveCoreContext.setUint(object, propertyKey, value); diff --git a/lib/src/rive_core/animation/keyframe_interpolation.dart b/lib/src/rive_core/animation/keyframe_interpolation.dart index 015c144..9ab7930 100644 --- a/lib/src/rive_core/animation/keyframe_interpolation.dart +++ b/lib/src/rive_core/animation/keyframe_interpolation.dart @@ -1 +1,12 @@ -enum KeyFrameInterpolation { hold, linear, cubic } +/// The type of interpolation used for a keyframe. +enum KeyFrameInterpolation { + /// Hold the incoming value until the next keyframe is reached. + hold, + + /// Linearly interpolate from the incoming to the outgoing value. + linear, + + /// Cubicly interpolate from incoming to outgoing value based on the + /// [CubicInterpolator]'s parameters. + cubic, +} diff --git a/lib/src/rive_core/animation/layer_state.dart b/lib/src/rive_core/animation/layer_state.dart index 9dfece1..7f3a1fb 100644 --- a/lib/src/rive_core/animation/layer_state.dart +++ b/lib/src/rive_core/animation/layer_state.dart @@ -1,4 +1,5 @@ import 'package:rive/src/core/core.dart'; +import 'package:rive/src/rive_core/animation/state_instance.dart'; import 'package:rive/src/rive_core/animation/state_machine_layer.dart'; import 'package:rive/src/rive_core/animation/state_transition.dart'; import 'package:rive/src/generated/animation/layer_state_base.dart'; @@ -7,11 +8,16 @@ export 'package:rive/src/generated/animation/layer_state_base.dart'; abstract class LayerState extends LayerStateBase { final StateTransitions _transitions = StateTransitions(); StateTransitions get transitions => _transitions; + @override void onAdded() {} + @override void onAddedDirty() {} + void internalAddTransition(StateTransition transition) { + assert(!_transitions.contains(transition), + 'shouldn\'t already contain the transition'); _transitions.add(transition); } @@ -24,6 +30,8 @@ abstract class LayerState extends LayerStateBase { super.onRemoved(); } + StateInstance makeInstance(); + @override bool import(ImportStack stack) { var importer = @@ -32,6 +40,7 @@ abstract class LayerState extends LayerStateBase { return false; } importer.addState(this); + return super.import(stack); } } diff --git a/lib/src/rive_core/animation/linear_animation.dart b/lib/src/rive_core/animation/linear_animation.dart index df94bef..b774e5e 100644 --- a/lib/src/rive_core/animation/linear_animation.dart +++ b/lib/src/rive_core/animation/linear_animation.dart @@ -1,4 +1,5 @@ import 'dart:collection'; + import 'package:rive/src/core/core.dart'; import 'package:rive/src/rive_core/animation/keyed_object.dart'; import 'package:rive/src/rive_core/animation/loop.dart'; @@ -7,8 +8,16 @@ import 'package:rive/src/generated/animation/linear_animation_base.dart'; export 'package:rive/src/generated/animation/linear_animation_base.dart'; class LinearAnimation extends LinearAnimationBase { + /// Map objectId to KeyedObject. N.B. this is the id of the object that we + /// want to key in core, not of the KeyedObject. It's a clear way to see if an + /// object is keyed in this animation. final _keyedObjects = HashMap(); + + /// The metadata for the objects that are keyed in this animation. Iterable get keyedObjects => _keyedObjects.values; + + /// Called by rive_core to add a KeyedObject to the animation. This should be + /// @internal when it's supported. bool internalAddKeyedObject(KeyedObject object) { if (internalCheckAddKeyedObject(object)) { _keyedObjects[object.objectId] = object; @@ -19,6 +28,9 @@ class LinearAnimation extends LinearAnimationBase { bool internalCheckAddKeyedObject(KeyedObject object) { var value = _keyedObjects[object.objectId]; + + // If the object is already keyed, that's ok just make sure the KeyedObject + // matches. if (value != null && value != object) { return false; } @@ -29,6 +41,13 @@ class LinearAnimation extends LinearAnimationBase { double get endSeconds => (enableWorkArea ? workEnd : duration).toDouble() / fps; double get durationSeconds => endSeconds - startSeconds; + + /// Pass in a different [core] context if you want to apply the animation to a + /// different instance. This isn't meant to be used yet but left as mostly a + /// note to remember that at runtime we have to support applying animations to + /// instances. We do a nice job of not duping all that data at runtime (so + /// animations exist once but entire Rive file can be instanced multiple times + /// playing different positions). void apply(double time, {required CoreContext coreContext, double mix = 1}) { for (final keyedObject in _keyedObjects.values) { keyedObject.apply(time, mix, coreContext); @@ -37,20 +56,28 @@ class LinearAnimation extends LinearAnimationBase { Loop get loop => Loop.values[loopValue]; set loop(Loop value) => loopValue = value.index; + @override void durationChanged(int from, int to) {} + @override void enableWorkAreaChanged(bool from, bool to) {} + @override void fpsChanged(int from, int to) {} + @override void loopValueChanged(int from, int to) {} + @override void speedChanged(double from, double to) {} + @override void workEndChanged(int from, int to) {} + @override void workStartChanged(int from, int to) {} + @override bool import(ImportStack stack) { var artboardImporter = stack.latest(ArtboardBase.typeKey); @@ -58,6 +85,7 @@ class LinearAnimation extends LinearAnimationBase { return false; } artboardImporter.addAnimation(this); + return super.import(stack); } } diff --git a/lib/src/rive_core/animation/linear_animation_instance.dart b/lib/src/rive_core/animation/linear_animation_instance.dart index 14655b2..49201ce 100644 --- a/lib/src/rive_core/animation/linear_animation_instance.dart +++ b/lib/src/rive_core/animation/linear_animation_instance.dart @@ -11,47 +11,73 @@ class LinearAnimationInstance { 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() / animation.fps; + + /// Note that when time is set, the direction will be changed to 1 set time(double value) { if (_time == value) { return; } + // Make sure to keep last and total in relative lockstep so state machines + // can track change even when setting time. var diff = _totalTime - _lastTotalTime; _time = _totalTime = value; _lastTotalTime = _totalTime - diff; _direction = 1; } + /// Returns the current time position of the animation in seconds double get time => _time; + + /// Direction should only be +1 or -1 set direction(int value) => _direction = value == -1 ? -1 : 1; + + /// Returns the animation's play direction: 1 for forwards, -1 for backwards int get direction => _direction; + + /// Returns the end time of the animation in seconds double get endTime => (animation.enableWorkArea ? animation.workEnd : animation.duration) .toDouble() / animation.fps; + + /// Returns the start time of the animation in seconds double get startTime => (animation.enableWorkArea ? animation.workStart : 0).toDouble() / animation.fps; + double get progress => (_time - startTime) / (endTime - startTime); + + /// Resets the animation to the starting frame void reset() => _time = startTime; + + /// Whether the controller driving this animation should keep requesting + /// frames be drawn. bool get keepGoing => animation.loop != Loop.oneShot || !_didLoop; + bool advance(double elapsedSeconds) { 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; var range = end - start; + bool keepGoing = true; _didLoop = false; _spilledTime = 0; + switch (animation.loop) { case Loop.oneShot: if (frames > end) { @@ -72,6 +98,7 @@ class LinearAnimationInstance { } break; case Loop.pingPong: + // ignore: literal_only_boolean_expressions while (true) { if (_direction == 1 && frames >= end) { _spilledTime = (frames - end) / animation.fps; @@ -86,6 +113,11 @@ class LinearAnimationInstance { _time = frames / animation.fps; _didLoop = true; } else { + // we're within the range, we can stop fixing. We do this in a + // loop to fix conditions when time has advanced so far that we've + // ping-ponged back and forth a few times in a single frame. We + // want to accomodate for this in cases where animations are not + // advanced on regular intervals. break; } } diff --git a/lib/src/rive_core/animation/loop.dart b/lib/src/rive_core/animation/loop.dart index c0f3046..702dc26 100644 --- a/lib/src/rive_core/animation/loop.dart +++ b/lib/src/rive_core/animation/loop.dart @@ -1 +1,12 @@ -enum Loop { oneShot, loop, pingPong } +/// Loop options for linear animations. +enum Loop { + /// Play until the duration or end of work area of the animation. + oneShot, + + /// Play until the duration or end of work area of the animation and then go + /// back to the start (0 seconds). + loop, + + /// Play to the end of the duration/work area and then play back. + pingPong, +} diff --git a/lib/src/rive_core/animation/state_instance.dart b/lib/src/rive_core/animation/state_instance.dart new file mode 100644 index 0000000..1707048 --- /dev/null +++ b/lib/src/rive_core/animation/state_instance.dart @@ -0,0 +1,33 @@ +import 'dart:collection'; + +import 'package:rive/src/core/core.dart'; +import 'package:rive/src/rive_core/animation/layer_state.dart'; + +/// Represents the instance of a [LayerState] which is being used in a +/// [LayerController] of a [StateMachineController]. Abstract representation of +/// an Animation (for [AnimationState]) or set of Animations in the case of a +/// [BlendState]. +abstract class StateInstance { + final LayerState state; + + StateInstance(this.state); + + void advance(double seconds, HashMap inputValues); + void apply(CoreContext core, double mix); + + bool get keepGoing; +} + +/// A single one of these is created per Layer which just represents/wraps the +/// AnyState but conforms to the instance interface. +class SystemStateInstance extends StateInstance { + SystemStateInstance(LayerState state) : super(state); + @override + void advance(double seconds, HashMap inputValues) {} + + @override + void apply(CoreContext core, double mix) {} + + @override + bool get keepGoing => false; +} diff --git a/lib/src/rive_core/animation/state_machine.dart b/lib/src/rive_core/animation/state_machine.dart index e4009a9..9fa71e4 100644 --- a/lib/src/rive_core/animation/state_machine.dart +++ b/lib/src/rive_core/animation/state_machine.dart @@ -10,6 +10,7 @@ class StateMachine extends StateMachineBase { StateMachineComponents(); final StateMachineComponents layers = StateMachineComponents(); + @override bool import(ImportStack stack) { var artboardImporter = stack.latest(ArtboardBase.typeKey); @@ -17,6 +18,7 @@ class StateMachine extends StateMachineBase { return false; } artboardImporter.addStateMachine(this); + return super.import(stack); } } diff --git a/lib/src/rive_core/animation/state_machine_bool.dart b/lib/src/rive_core/animation/state_machine_bool.dart index 36601c3..3288484 100644 --- a/lib/src/rive_core/animation/state_machine_bool.dart +++ b/lib/src/rive_core/animation/state_machine_bool.dart @@ -4,8 +4,10 @@ export 'package:rive/src/generated/animation/state_machine_bool_base.dart'; class StateMachineBool extends StateMachineBoolBase { @override void valueChanged(bool from, bool to) {} + @override bool isValidType() => T == bool; + @override dynamic get controllerValue => value; } diff --git a/lib/src/rive_core/animation/state_machine_component.dart b/lib/src/rive_core/animation/state_machine_component.dart index 710bf83..f98295d 100644 --- a/lib/src/rive_core/animation/state_machine_component.dart +++ b/lib/src/rive_core/animation/state_machine_component.dart @@ -1,9 +1,11 @@ import 'dart:collection'; + import 'package:rive/src/core/core.dart'; import 'package:rive/src/rive_core/animation/state_machine.dart'; import 'package:rive/src/generated/animation/state_machine_component_base.dart'; export 'package:rive/src/generated/animation/state_machine_component_base.dart'; +/// Implemented by state machine inputs and layers. abstract class StateMachineComponent extends StateMachineComponentBase { StateMachine? _stateMachine; StateMachine? get stateMachine => _stateMachine; @@ -15,16 +17,22 @@ abstract class StateMachineComponent extends StateMachineComponentBase { machineComponentList(_stateMachine!).remove(this); } _stateMachine = machine; + if (_stateMachine != null) { machineComponentList(_stateMachine!).add(this); } } + // Intentionally using ListBase instead of FractionallyIndexedList here as + // it's more compatible with runtime. ListBase machineComponentList(StateMachine machine); + @override void nameChanged(String from, String to) {} + @override void onAddedDirty() {} + @override void onRemoved() { super.onRemoved(); @@ -39,6 +47,7 @@ abstract class StateMachineComponent extends StateMachineComponentBase { return false; } importer.addMachineComponent(this); + return super.import(importStack); } } diff --git a/lib/src/rive_core/animation/state_machine_input.dart b/lib/src/rive_core/animation/state_machine_input.dart index 2f9ff4b..ef801d8 100644 --- a/lib/src/rive_core/animation/state_machine_input.dart +++ b/lib/src/rive_core/animation/state_machine_input.dart @@ -1,4 +1,5 @@ import 'dart:collection'; + import 'package:rive/src/rive_core/animation/state_machine.dart'; import 'package:rive/src/rive_core/animation/state_machine_component.dart'; import 'package:rive/src/generated/animation/state_machine_input_base.dart'; @@ -6,9 +7,11 @@ export 'package:rive/src/generated/animation/state_machine_input_base.dart'; abstract class StateMachineInput extends StateMachineInputBase { static final StateMachineInput unknown = _StateMachineUnknownInput(); + @override ListBase machineComponentList(StateMachine machine) => machine.inputs; + bool isValidType() => false; dynamic get controllerValue => null; } diff --git a/lib/src/rive_core/animation/state_machine_layer.dart b/lib/src/rive_core/animation/state_machine_layer.dart index f9caa2c..c9f93b5 100644 --- a/lib/src/rive_core/animation/state_machine_layer.dart +++ b/lib/src/rive_core/animation/state_machine_layer.dart @@ -1,4 +1,5 @@ import 'dart:collection'; + import 'package:rive/src/rive_core/animation/any_state.dart'; import 'package:rive/src/rive_core/animation/entry_state.dart'; import 'package:rive/src/rive_core/animation/exit_state.dart'; @@ -13,12 +14,17 @@ class StateMachineLayer extends StateMachineLayerBase { LayerState? _entryState; LayerState? _anyState; LayerState? _exitState; + LayerState? get entryState => _entryState; LayerState? get anyState => _anyState; LayerState? get exitState => _exitState; + @override ListBase machineComponentList(StateMachine machine) => machine.layers; + + /// Called by rive_core to add a LayerState to the StateMachineLayer. This + /// should be @internal when it's supported. bool internalAddState(LayerState state) { switch (state.coreType) { case AnyStateBase.typeKey: @@ -31,6 +37,7 @@ class StateMachineLayer extends StateMachineLayerBase { _entryState = state; break; } + return true; } } diff --git a/lib/src/rive_core/animation/state_machine_layer_component.dart b/lib/src/rive_core/animation/state_machine_layer_component.dart index 0b41057..34e9851 100644 --- a/lib/src/rive_core/animation/state_machine_layer_component.dart +++ b/lib/src/rive_core/animation/state_machine_layer_component.dart @@ -1,4 +1,9 @@ +// We really want this file to import core for the flutter runtime, so make the +// linter happy... + +// ignore: unused_import import 'package:rive/src/core/core.dart'; + import 'package:rive/src/generated/animation/state_machine_layer_component_base.dart'; export 'package:rive/src/generated/animation/state_machine_layer_component_base.dart'; diff --git a/lib/src/rive_core/animation/state_machine_number.dart b/lib/src/rive_core/animation/state_machine_number.dart index 26dfe13..db019db 100644 --- a/lib/src/rive_core/animation/state_machine_number.dart +++ b/lib/src/rive_core/animation/state_machine_number.dart @@ -4,8 +4,10 @@ export 'package:rive/src/generated/animation/state_machine_number_base.dart'; class StateMachineNumber extends StateMachineNumberBase { @override void valueChanged(double from, double to) {} + @override bool isValidType() => T == double; + @override dynamic get controllerValue => value; } diff --git a/lib/src/rive_core/animation/state_machine_trigger.dart b/lib/src/rive_core/animation/state_machine_trigger.dart index b265ced..805112a 100644 --- a/lib/src/rive_core/animation/state_machine_trigger.dart +++ b/lib/src/rive_core/animation/state_machine_trigger.dart @@ -4,6 +4,7 @@ export 'package:rive/src/generated/animation/state_machine_trigger_base.dart'; class StateMachineTrigger extends StateMachineTriggerBase { bool _triggered = false; bool get triggered => _triggered; + void fire() { _triggered = true; } @@ -14,6 +15,7 @@ class StateMachineTrigger extends StateMachineTriggerBase { @override bool isValidType() => T == bool; + @override dynamic get controllerValue => _triggered; } diff --git a/lib/src/rive_core/animation/state_transition.dart b/lib/src/rive_core/animation/state_transition.dart index 6ff0b02..32dc7b3 100644 --- a/lib/src/rive_core/animation/state_transition.dart +++ b/lib/src/rive_core/animation/state_transition.dart @@ -1,24 +1,40 @@ +import 'dart:collection'; + import 'package:rive/src/core/core.dart'; import 'package:rive/src/rive_core/animation/animation_state.dart'; +import 'package:rive/src/rive_core/animation/animation_state_instance.dart'; import 'package:rive/src/rive_core/animation/layer_state.dart'; +import 'package:rive/src/rive_core/animation/linear_animation.dart'; +import 'package:rive/src/rive_core/animation/linear_animation_instance.dart'; +import 'package:rive/src/rive_core/animation/state_instance.dart'; import 'package:rive/src/rive_core/animation/transition_condition.dart'; +import 'package:rive/src/rive_core/animation/transition_trigger_condition.dart'; import 'package:rive/src/generated/animation/state_transition_base.dart'; import 'package:rive/src/rive_core/state_transition_flags.dart'; export 'package:rive/src/generated/animation/state_transition_base.dart'; +enum AllowTransition { no, waitingForExit, yes } + class StateTransition extends StateTransitionBase { final StateTransitionConditions conditions = StateTransitionConditions(); LayerState? stateTo; static final StateTransition unknown = StateTransition(); + @override bool validate() { - return super.validate() && stateTo != null; + return super.validate() && + + // need this last so runtimes get it which makes the whole + // allowTransitionFrom thing above a little weird. + stateTo != null; } @override void onAdded() {} + @override void onAddedDirty() {} + @override void onRemoved() { super.onRemoved(); @@ -27,6 +43,11 @@ 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; + + /// The amount of time to mix the outgoing animation onto the incoming one + /// when changing state. Only applies when going out from an AnimationState. + /// [stateFrom] must be provided as at runtime we don't store the reference to + /// the state this transition comes from. double mixTime(LayerState stateFrom) { if (duration == 0) { return 0; @@ -42,14 +63,30 @@ class StateTransition extends StateTransitionBase { } } + /// Provide the animation instance to use for computing percentage durations + /// for exit time. + LinearAnimationInstance? exitTimeAnimationInstance(StateInstance stateFrom) => + stateFrom is AnimationStateInstance ? stateFrom.animationInstance : null; + + /// Provide the animation to use for computing percentage durations for exit + /// time. + LinearAnimation? exitTimeAnimation(LayerState stateFrom) => + stateFrom is AnimationState ? stateFrom.animation : null; + + /// Computes the exit time in seconds of the [stateFrom]. Set [absolute] to + /// true if you want the returned time to be relative to the entire animation. + /// Set [absolute] to false if you want it relative to the work area. double exitTimeSeconds(LayerState stateFrom, {bool absolute = false}) { if ((flags & StateTransitionFlags.exitTimeIsPercentage) != 0) { var animationDuration = 0.0; var start = 0.0; - if (stateFrom is AnimationState) { - start = absolute ? stateFrom.animation?.startSeconds ?? 0 : 0; - animationDuration = stateFrom.animation?.durationSeconds ?? 0; + + var exitAnimation = exitTimeAnimation(stateFrom); + if (exitAnimation != null) { + start = absolute ? exitAnimation.startSeconds : 0; + animationDuration = exitAnimation.durationSeconds; } + return start + exitTime / 100 * animationDuration; } else { return exitTime / 1000; @@ -64,28 +101,89 @@ class StateTransition extends StateTransitionBase { return false; } importer.addTransition(this); + return super.import(importStack); } + /// Called by rive_core to add a [TransitionCondition] to this + /// [StateTransition]. This should be @internal when it's supported. bool internalAddCondition(TransitionCondition condition) { if (conditions.contains(condition)) { return false; } conditions.add(condition); + return true; } + /// Called by rive_core to remove a [TransitionCondition] from this + /// [StateTransition]. This should be @internal when it's supported. bool internalRemoveCondition(TransitionCondition condition) { var removed = conditions.remove(condition); + return removed; } @override void flagsChanged(int from, int to) {} + @override void durationChanged(int from, int to) {} + @override void exitTimeChanged(int from, int to) {} + @override void stateToIdChanged(int from, int to) {} + + /// Returns true when this transition can be taken from [stateFrom] with the + /// given [inputValues]. + AllowTransition allowed(StateInstance stateFrom, + HashMap inputValues, bool ignoreTriggers) { + if (isDisabled) { + return AllowTransition.no; + } + for (final condition in conditions) { + if ((ignoreTriggers && condition is TransitionTriggerCondition) || + !condition.evaluate(inputValues)) { + return AllowTransition.no; + } + } + // For now we only enable exit time from AnimationStates, do we want to + // enable this for BlendStates? How would that work? + if (enableExitTime) { + var exitAnimation = exitTimeAnimationInstance(stateFrom); + if (exitAnimation != null) { + // Exit time is specified in a value less than a single loop, so we + // want to allow exiting regardless of which loop we're on. To do that + // we bring the exit time up to the loop our lastTime is at. + var lastTime = exitAnimation.lastTotalTime; + var time = exitAnimation.totalTime; + var exitTime = exitTimeSeconds(stateFrom.state); + var animationFrom = exitAnimation.animation; + if (exitTime < animationFrom.durationSeconds) { + // Get exit time relative to the loop lastTime was in. + exitTime += (lastTime / animationFrom.durationSeconds).floor() * + animationFrom.durationSeconds; + } + + if (time < exitTime) { + return AllowTransition.waitingForExit; + } + } + } + return AllowTransition.yes; + } + + bool applyExitCondition(StateInstance stateFrom) { + // Hold exit time when the user has set to pauseOnExit on this condition + // (only valid when exiting from an Animation). + bool useExitTime = enableExitTime && stateFrom is AnimationStateInstance; + if (pauseOnExit && useExitTime) { + stateFrom.animationInstance.time = + exitTimeSeconds(stateFrom.state, absolute: true); + return true; + } + return useExitTime; + } } diff --git a/lib/src/rive_core/animation/transition_bool_condition.dart b/lib/src/rive_core/animation/transition_bool_condition.dart index 3161e0d..5e58c47 100644 --- a/lib/src/rive_core/animation/transition_bool_condition.dart +++ b/lib/src/rive_core/animation/transition_bool_condition.dart @@ -7,6 +7,7 @@ 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) { if (input is! StateMachineBool) { diff --git a/lib/src/rive_core/animation/transition_condition.dart b/lib/src/rive_core/animation/transition_condition.dart index 79bb752..d5f8251 100644 --- a/lib/src/rive_core/animation/transition_condition.dart +++ b/lib/src/rive_core/animation/transition_condition.dart @@ -1,4 +1,5 @@ import 'dart:collection'; + import 'package:rive/src/core/core.dart'; import 'package:rive/src/rive_core/animation/state_machine_input.dart'; import 'package:rive/src/rive_core/animation/state_transition.dart'; @@ -11,7 +12,7 @@ enum TransitionConditionOp { lessThanOrEqual, greaterThanOrEqual, lessThan, - greaterThan + greaterThan, } abstract class TransitionCondition extends TransitionConditionBase { @@ -21,7 +22,9 @@ abstract class TransitionCondition extends TransitionConditionBase { if (_input == value) { return; } + _input = value; + inputId = _input.id; } @@ -32,12 +35,14 @@ abstract class TransitionCondition extends TransitionConditionBase { @override void onAdded() {} + @override void onAddedDirty() { input = context.resolveWithDefault(inputId, StateMachineInput.unknown); } bool evaluate(HashMap values); + @override bool import(ImportStack importStack) { var importer = importStack @@ -46,6 +51,7 @@ abstract class TransitionCondition extends TransitionConditionBase { return false; } importer.addCondition(this); + return super.import(importStack); } } diff --git a/lib/src/rive_core/animation/transition_number_condition.dart b/lib/src/rive_core/animation/transition_number_condition.dart index a7fb680..44a9dc4 100644 --- a/lib/src/rive_core/animation/transition_number_condition.dart +++ b/lib/src/rive_core/animation/transition_number_condition.dart @@ -1,4 +1,5 @@ import 'dart:collection'; + import 'package:rive/src/rive_core/animation/state_machine_number.dart'; import 'package:rive/src/rive_core/animation/transition_condition.dart'; import 'package:rive/src/generated/animation/transition_number_condition_base.dart'; @@ -7,8 +8,10 @@ export 'package:rive/src/generated/animation/transition_number_condition_base.da class TransitionNumberCondition extends TransitionNumberConditionBase { @override void valueChanged(double from, double to) {} + @override bool validate() => super.validate() && (input is StateMachineNumber); + @override bool evaluate(HashMap values) { if (input is! StateMachineNumber) { diff --git a/lib/src/rive_core/animation/transition_trigger_condition.dart b/lib/src/rive_core/animation/transition_trigger_condition.dart index 670c29a..124a2e2 100644 --- a/lib/src/rive_core/animation/transition_trigger_condition.dart +++ b/lib/src/rive_core/animation/transition_trigger_condition.dart @@ -1,4 +1,5 @@ import 'dart:collection'; + import 'package:rive/src/rive_core/animation/state_machine_trigger.dart'; import 'package:rive/src/generated/animation/transition_trigger_condition_base.dart'; export 'package:rive/src/generated/animation/transition_trigger_condition_base.dart'; @@ -6,6 +7,7 @@ export 'package:rive/src/generated/animation/transition_trigger_condition_base.d class TransitionTriggerCondition extends TransitionTriggerConditionBase { @override bool validate() => super.validate() && (input is StateMachineTrigger); + @override bool evaluate(HashMap values) { if (input is! StateMachineTrigger) { @@ -16,6 +18,7 @@ class TransitionTriggerCondition extends TransitionTriggerConditionBase { values[input.id] = false; return true; } + var triggerInput = input as StateMachineTrigger; if (triggerInput.triggered) { return true; diff --git a/lib/src/rive_core/animation/transition_value_condition.dart b/lib/src/rive_core/animation/transition_value_condition.dart index 9c6fd1b..2a68b87 100644 --- a/lib/src/rive_core/animation/transition_value_condition.dart +++ b/lib/src/rive_core/animation/transition_value_condition.dart @@ -4,6 +4,9 @@ export 'package:rive/src/generated/animation/transition_value_condition_base.dar abstract class TransitionValueCondition extends TransitionValueConditionBase { TransitionConditionOp get op => TransitionConditionOp.values[opValue]; + @override - void opValueChanged(int from, int to) {} + void opValueChanged(int from, int to) { + // TODO: implement opValueChanged + } } diff --git a/lib/src/rive_core/artboard.dart b/lib/src/rive_core/artboard.dart index a31751b..b9b45b5 100644 --- a/lib/src/rive_core/artboard.dart +++ b/lib/src/rive_core/artboard.dart @@ -1,4 +1,5 @@ import 'dart:ui'; + import 'package:rive/src/core/core.dart'; import 'package:rive/src/rive_core/animation/animation.dart'; import 'package:rive/src/rive_core/component.dart'; @@ -12,34 +13,52 @@ import 'package:rive/src/rive_core/rive_animation_controller.dart'; import 'package:rive/src/rive_core/shapes/paint/shape_paint_mutator.dart'; import 'package:rive/src/rive_core/shapes/shape_paint_container.dart'; import 'package:rive/src/utilities/dependency_sorter.dart'; + import 'package:rive/src/generated/artboard_base.dart'; + export 'package:rive/src/generated/artboard_base.dart'; class Artboard extends ArtboardBase with ShapePaintContainer { + /// Artboard are one of the few (only?) components that can be orphaned. @override bool get canBeOrphaned => true; + final Path path = Path(); List _dependencyOrder = []; final List _drawables = []; final List _rules = []; List _sortedDrawRules = []; + final Set _components = {}; + List get drawables => _drawables; + final AnimationList _animations = AnimationList(); + + /// List of animations in this artboard. AnimationList get animations => _animations; + + /// Does this artboard have animations? bool get hasAnimations => _animations.isNotEmpty; + int _dirtDepth = 0; int _dirt = 255; + void forEachComponent(void Function(Component) callback) => _components.forEach(callback); + @override Artboard get artboard => this; + Vec2D get originWorld { return Vec2D.fromValues(x + width * originX, y + height * originY); } + /// Walk the dependency tree and update components in order. Returns true if + /// any component updated. bool updateComponents() { bool didUpdate = false; + if ((_dirt & ComponentDirt.drawOrder) != 0) { sortDrawOrder(); _dirt &= ~ComponentDirt.drawOrder; @@ -51,6 +70,8 @@ class Artboard extends ArtboardBase with ShapePaintContainer { int count = _dependencyOrder.length; while ((_dirt & ComponentDirt.components) != 0 && step < maxSteps) { _dirt &= ~ComponentDirt.components; + // Track dirt depth here so that if something else marks + // dirty, we restart. for (int i = 0; i < count; i++) { Component component = _dependencyOrder[i]; _dirtDepth = i; @@ -71,6 +92,7 @@ class Artboard extends ArtboardBase with ShapePaintContainer { return didUpdate; } + /// Update any dirty components in this artboard. bool advance(double elapsedSeconds) { bool didUpdate = false; for (final controller in _animationControllers) { @@ -93,6 +115,10 @@ class Artboard extends ArtboardBase with ShapePaintContainer { context.markNeedsAdvance(); _dirt |= ComponentDirt.components; } + + /// If the order of the component is less than the current dirt depth, + /// update the dirt depth so that the update loop can break out early and + /// re-run (something up the tree is dirty). if (component.graphOrder < _dirtDepth) { _dirtDepth = component.graphOrder; } @@ -100,17 +126,24 @@ class Artboard extends ArtboardBase with ShapePaintContainer { @override bool resolveArtboard() => true; + + /// Sort the DAG for resolution in order of dependencies such that dependent + /// compnents process after their dependencies. void sortDependencies() { var optimistic = DependencySorter(); var order = optimistic.sort(this); if (order.isEmpty) { + // cycle detected, use a more robust solver var robust = TarjansDependencySorter(); order = robust.sort(this); } + _dependencyOrder = order; for (final component in _dependencyOrder) { component.graphOrder = graphOrder++; + // component.dirt = 255; } + _dirt |= ComponentDirt.components; } @@ -145,16 +178,23 @@ class Artboard extends ArtboardBase with ShapePaintContainer { return Vec2D.add(Vec2D(), worldTranslation, wt); } + /// Adds a component to the artboard. Good place for the artboard to check for + /// components it'll later need to do stuff with (like draw them or sort them + /// when the draw order changes). void addComponent(Component component) { if (!_components.add(component)) { return; } } + /// Remove a component from the artboard and its various tracked lists of + /// components. void removeComponent(Component component) { _components.remove(component); } + /// Let the artboard know that the drawables need to be resorted before + /// drawing next. void markDrawOrderDirty() { if ((dirt & ComponentDirt.drawOrder) == 0) { context.markNeedsAdvance(); @@ -162,13 +202,25 @@ class Artboard extends ArtboardBase with ShapePaintContainer { } } + /// Draw the drawable components in this artboard. void draw(Canvas canvas) { canvas.save(); canvas.clipRect(Rect.fromLTWH(0, 0, width, height)); + // Get into artboard's world space. This is because the artboard draws + // components in the artboard's space (in component lingo we call this world + // space). The artboards themselves are drawn in the editor's world space, + // which is the world space that is used by stageItems. This is a little + // confusing and perhaps we should find a better wording for the transform + // spaces. We used "world space" in components as that's the game engine + // ratified way of naming the top-most transformation. Perhaps we should + // rename those to artboardTransform and worldTransform is only reserved for + // stageItems? The other option is to stick with 'worldTransform' in + // components and use 'editor or stageTransform' for stageItems. canvas.translate(width * originX, height * originY); for (final fill in fills) { fill.draw(canvas, path); } + for (var drawable = _firstDrawable; drawable != null; drawable = drawable.prev) { @@ -180,8 +232,10 @@ class Artboard extends ArtboardBase with ShapePaintContainer { canvas.restore(); } + /// Our world transform is always the identity. Artboard defines world space. @override Mat2D get worldTransform => Mat2D(); + @override void originXChanged(double from, double to) { addDirt(ComponentDirt.worldTransform); @@ -192,20 +246,31 @@ class Artboard extends ArtboardBase with ShapePaintContainer { addDirt(ComponentDirt.worldTransform); } + /// Called by rive_core to add an Animation to an Artboard. This should be + /// @internal when it's supported. bool internalAddAnimation(Animation animation) { if (_animations.contains(animation)) { return false; } _animations.add(animation); + return true; } + /// Called by rive_core to remove an Animation from an Artboard. This should + /// be @internal when it's supported. bool internalRemoveAnimation(Animation animation) { bool removed = _animations.remove(animation); + return removed; } + /// The animation controllers that are called back whenever the artboard + /// advances. final Set _animationControllers = {}; + + /// Add an animation controller to this artboard. Playing will be scheduled if + /// it's already playing. bool addController(RiveAnimationController controller) { if (_animationControllers.contains(controller) || !controller.init(context)) { @@ -219,6 +284,7 @@ class Artboard extends ArtboardBase with ShapePaintContainer { return true; } + /// Remove an animation controller form this artboard. bool removeController(RiveAnimationController controller) { if (_animationControllers.remove(controller)) { controller.isActiveChanged.removeListener(_onControllerPlayingChanged); @@ -229,25 +295,37 @@ class Artboard extends ArtboardBase with ShapePaintContainer { } void _onControllerPlayingChanged() => context.markNeedsAdvance(); + @override void onFillsChanged() {} + @override void onPaintMutatorChanged(ShapePaintMutator mutator) {} + @override void onStrokesChanged() {} + @override Vec2D get worldTranslation => Vec2D(); + Drawable? _firstDrawable; + void computeDrawOrder() { _drawables.clear(); _rules.clear(); buildDrawOrder(_drawables, null, _rules); + + // Build rule dependencies. In practice this'll need to happen anytime a + // target drawable is changed or rule is added/removed. var root = DrawTarget(); + // Make sure all dependents are empty. for (final nodeRules in _rules) { for (final target in nodeRules.targets) { target.dependents.clear(); } } + + // Now build up the dependencies. for (final nodeRules in _rules) { for (final target in nodeRules.targets) { root.dependents.add(target); @@ -259,19 +337,25 @@ class Artboard extends ArtboardBase with ShapePaintContainer { } } } + var sorter = DependencySorter(); + _sortedDrawRules = sorter.sort(root).cast().skip(1).toList(); + sortDrawOrder(); } void sortDrawOrder() { + // Clear out rule first/last items. for (final rule in _sortedDrawRules) { rule.first = rule.last = null; } + _firstDrawable = null; Drawable? lastDrawable; for (final drawable in _drawables) { var rules = drawable.flattenedDrawRules; + var target = rules?.activeTarget; if (target != null) { if (target.first == null) { @@ -294,6 +378,7 @@ class Artboard extends ArtboardBase with ShapePaintContainer { } } } + for (final rule in _sortedDrawRules) { if (rule.first == null) { continue; @@ -323,6 +408,7 @@ class Artboard extends ArtboardBase with ShapePaintContainer { break; } } + _firstDrawable = lastDrawable; } } diff --git a/lib/src/rive_core/backboard.dart b/lib/src/rive_core/backboard.dart index ebda29f..5b3607a 100644 --- a/lib/src/rive_core/backboard.dart +++ b/lib/src/rive_core/backboard.dart @@ -3,8 +3,10 @@ export 'package:rive/src/generated/backboard_base.dart'; class Backboard extends BackboardBase { static final Backboard unknown = Backboard(); + @override void onAdded() {} + @override void onAddedDirty() {} } diff --git a/lib/src/rive_core/bones/bone.dart b/lib/src/rive_core/bones/bone.dart index 8dd6359..57e2885 100644 --- a/lib/src/rive_core/bones/bone.dart +++ b/lib/src/rive_core/bones/bone.dart @@ -22,6 +22,9 @@ class Bone extends BoneBase { return null; } + /// Iterate through the child bones. [BoneCallback] returns false if iteration + /// can stop. Returns false if iteration stopped, true if it made it through + /// the whole list. bool forEachBone(BoneCallback callback) { for (final child in children) { if (child.coreType == BoneBase.typeKey) { @@ -35,6 +38,7 @@ class Bone extends BoneBase { @override double get x => (parent as Bone).length; + @override set x(double value) { throw UnsupportedError('not expected to set x on a bone.'); @@ -42,6 +46,7 @@ class Bone extends BoneBase { @override double get y => 0; + @override set y(double value) { throw UnsupportedError('not expected to set y on a bone.'); @@ -49,6 +54,9 @@ class Bone extends BoneBase { @override bool validate() { + // Bones are only valid if they're parented to other bones. RootBones are a + // special case, but they inherit from bone so we check the concrete type + // here to make sure we evalute this check only for non-root bones. return super.validate() && (coreType != BoneBase.typeKey || parent is Bone); } } diff --git a/lib/src/rive_core/bones/cubic_weight.dart b/lib/src/rive_core/bones/cubic_weight.dart index f673ec0..45a0ee0 100644 --- a/lib/src/rive_core/bones/cubic_weight.dart +++ b/lib/src/rive_core/bones/cubic_weight.dart @@ -5,12 +5,16 @@ export 'package:rive/src/generated/bones/cubic_weight_base.dart'; class CubicWeight extends CubicWeightBase { final Vec2D inTranslation = Vec2D(); final Vec2D outTranslation = Vec2D(); + @override void inIndicesChanged(int from, int to) {} + @override void inValuesChanged(int from, int to) {} + @override void outIndicesChanged(int from, int to) {} + @override void outValuesChanged(int from, int to) {} } diff --git a/lib/src/rive_core/bones/skin.dart b/lib/src/rive_core/bones/skin.dart index 1f8fba6..55dc1fe 100644 --- a/lib/src/rive_core/bones/skin.dart +++ b/lib/src/rive_core/bones/skin.dart @@ -1,24 +1,37 @@ import 'dart:typed_data'; + import 'package:rive/src/rive_core/bones/skinnable.dart'; import 'package:rive/src/rive_core/bones/tendon.dart'; import 'package:rive/src/rive_core/component.dart'; import 'package:rive/src/rive_core/math/mat2d.dart'; import 'package:rive/src/rive_core/shapes/path_vertex.dart'; + import 'package:rive/src/generated/bones/skin_base.dart'; export 'package:rive/src/generated/bones/skin_base.dart'; +/// Represents a skin deformation of either a Path or an Image Mesh connected to +/// a set of bones. class Skin extends SkinBase { final List _tendons = []; List get tendons => _tendons; Float32List _boneTransforms = Float32List(0); final Mat2D _worldTransform = Mat2D(); + @override void onDirty(int mask) { + // When the skin is dirty the deformed skinnable will need to regenerate its + // drawing commands. + + // TODO: rename path to topology/surface something common between path & + // mesh. (parent as Skinnable).markSkinDirty(); } @override void update(int dirt) { + // Any dirt here indicates that the transforms needs to be rebuilt. This + // should only be worldTransform from the bones (recursively passed down) or + // ComponentDirt.path from the PointsPath (set explicitly). var size = (_tendons.length + 1) * 6; if (_boneTransforms.length != size) { _boneTransforms = Float32List(size); @@ -29,6 +42,7 @@ class Skin extends SkinBase { _boneTransforms[4] = 0; _boneTransforms[5] = 0; } + var temp = Mat2D(); var bidx = 6; for (final tendon in _tendons) { @@ -79,6 +93,8 @@ class Skin extends SkinBase { @override void buildDependencies() { super.buildDependencies(); + // A skin depends on all its bones. N.B. that we don't depend on the parent + // skinnable. The skinnable depends on us. for (final tendon in _tendons) { tendon.bone?.addDependent(this); } @@ -110,6 +126,7 @@ class Skin extends SkinBase { markRebuildDependencies(); } parent?.markRebuildDependencies(); + break; } } diff --git a/lib/src/rive_core/bones/skinnable.dart b/lib/src/rive_core/bones/skinnable.dart index 4054abd..36fd0a5 100644 --- a/lib/src/rive_core/bones/skinnable.dart +++ b/lib/src/rive_core/bones/skinnable.dart @@ -1,18 +1,27 @@ import 'package:rive/src/rive_core/bones/skin.dart'; import 'package:rive/src/rive_core/component.dart'; +/// An abstraction to give a common interface to any container component that +/// can contain a skin to bind bones to. abstract class Skinnable { + // _skin is null when this object isn't connected to bones. Skin? _skin; Skin? get skin => _skin; + void appendChild(Component child); + + // ignore: use_setters_to_change_properties void addSkin(Skin skin) { + // Notify old skin/maybe support multiple skins in the future? _skin = skin; + markSkinDirty(); } void removeSkin(Skin skin) { if (_skin == skin) { _skin = null; + markSkinDirty(); } } diff --git a/lib/src/rive_core/bones/tendon.dart b/lib/src/rive_core/bones/tendon.dart index 05466a1..84a3d14 100644 --- a/lib/src/rive_core/bones/tendon.dart +++ b/lib/src/rive_core/bones/tendon.dart @@ -8,6 +8,7 @@ class Tendon extends TendonBase { Mat2D? _inverseBind; SkeletalComponent? _bone; SkeletalComponent? get bone => _bone; + Mat2D get inverseBind { if (_inverseBind == null) { _inverseBind = Mat2D(); @@ -17,11 +18,16 @@ class Tendon extends TendonBase { } @override - void boneIdChanged(int from, int to) {} + void boneIdChanged(int from, int to) { + // This never happens, or at least it should only happen prior to an + // onAddedDirty call. + } + @override void onAddedDirty() { super.onAddedDirty(); _bone = context.resolve(boneId); + _bind[0] = xx; _bind[1] = xy; _bind[2] = yx; @@ -32,6 +38,7 @@ class Tendon extends TendonBase { @override void update(int dirt) {} + @override void txChanged(double from, double to) { _bind[4] = to; diff --git a/lib/src/rive_core/bones/weight.dart b/lib/src/rive_core/bones/weight.dart index a262d67..af4cb82 100644 --- a/lib/src/rive_core/bones/weight.dart +++ b/lib/src/rive_core/bones/weight.dart @@ -1,4 +1,5 @@ import 'dart:typed_data'; + import 'package:rive/src/rive_core/math/mat2d.dart'; import 'package:rive/src/rive_core/math/vec2d.dart'; import 'package:rive/src/generated/bones/weight_base.dart'; @@ -6,12 +7,18 @@ export 'package:rive/src/generated/bones/weight_base.dart'; class Weight extends WeightBase { final Vec2D translation = Vec2D(); + @override void indicesChanged(int from, int to) {} + @override - void update(int dirt) {} + void update(int dirt) { + // Intentionally empty. Weights don't update. + } + @override void valuesChanged(int from, int to) {} + static void deform(double x, double y, int indices, int weights, Mat2D world, Float32List boneTransforms, Vec2D result) { double xx = 0, xy = 0, yx = 0, yy = 0, tx = 0, ty = 0; @@ -22,6 +29,7 @@ class Weight extends WeightBase { if (weight == 0) { continue; } + double normalizedWeight = weight / 255; var index = encodedWeightValue(i, indices); var startBoneTransformIndex = index * 6; diff --git a/lib/src/rive_core/bones/weighted_vertex.dart b/lib/src/rive_core/bones/weighted_vertex.dart index ec0aa30..473cd67 100644 --- a/lib/src/rive_core/bones/weighted_vertex.dart +++ b/lib/src/rive_core/bones/weighted_vertex.dart @@ -1,19 +1,31 @@ +/// Helper to abstract changing weighted values on a vertex. abstract class WeightedVertex { int get weights; int get weightIndices; set weights(int value); set weightIndices(int value); + + /// Set the weight of this vertex for a specific tendon. void setWeight(int tendonIndex, int tendonCount, double weight) { int tendonWeightIndex = _setTendonWeight(tendonIndex, (weight.clamp(0, 1) * 255).round()); + + // re-normalize the list such that only bones with value are at the + // start and they sum to 100%, if any need to change make sure to give + // priority (not change) tendonIndex which we just tried to set. + var tendonWeights = _tendonWeights; int totalWeight = tendonWeights.fold( 0, (value, tendonWeight) => value + tendonWeight.weight); var vertexTendons = tendonWeights.where((tendonWeight) => tendonWeight.tendon != 0); + const maxWeight = 255; + var remainder = maxWeight - totalWeight; if (vertexTendons.length == 1) { + // User is specifically setting a single tendon to a value, just pick + // the next one up (modulate by the total number of tendons). var patchTendonIndex = (tendonIndex + 1) % tendonCount; _setTendonWeight( patchTendonIndex, tendonCount == 1 ? maxWeight : remainder); @@ -40,6 +52,8 @@ abstract class WeightedVertex { void _sortWeights() { var tendonWeights = _tendonWeights; + // Sort weights such that tendons with value show up first and any with no + // value (0 weight) are cleared to the 0 (no) tendon. tendonWeights.sort((a, b) => b.weight.compareTo(a.weight)); for (int i = 0; i < tendonWeights.length; i++) { final tw = tendonWeights[i]; @@ -54,14 +68,16 @@ abstract class WeightedVertex { _WeightHelper(2, (weightIndices >> 16) & 0xFF, _getRawWeight(2)), _WeightHelper(3, (weightIndices >> 24) & 0xFF, _getRawWeight(3)) ]; + int _setTendonWeight(int tendonIndex, int weight) { var indices = weightIndices; var bonesIndices = [ indices & 0xFF, (indices >> 8) & 0xFF, (indices >> 16) & 0xFF, - (indices >> 24) & 0xFF + (indices >> 24) & 0xFF, ]; + int setWeightIndex = -1; for (int i = 0; i < 4; i++) { if (bonesIndices[i] == tendonIndex + 1) { @@ -70,10 +86,14 @@ abstract class WeightedVertex { break; } } + + // This bone wasn't weighted for this vertex, go find the bone with the + // least weight (or a 0 bone) and use it. if (setWeightIndex == -1) { int lowestWeight = double.maxFinite.toInt(); for (int i = 0; i < 4; i++) { if (bonesIndices[i] == 0) { + // this isn't set to a bone yet, use it! setWeightIndex = i; break; } @@ -83,16 +103,21 @@ abstract class WeightedVertex { lowestWeight = weight; } } + _setTendonIndex(setWeightIndex, tendonIndex + 1); _rawSetWeight(setWeightIndex, weight); } return setWeightIndex; } + /// [tendonIndex] of 0 means no bound tendon, when bound to an actual tendon, + /// it should be set to the skin's tendon's index + 1. void _setTendonIndex(int weightIndex, int tendonIndex) { assert(weightIndex < 4 && weightIndex >= 0); var indexValues = weightIndices; + // Clear the bits for this weight value. indexValues &= ~(0xFF << (weightIndex * 8)); + // Set the bits for this weight value. weightIndices = indexValues | (tendonIndex << (weightIndex * 8)); } @@ -104,11 +129,14 @@ abstract class WeightedVertex { void _rawSetWeight(int weightIndex, int weightValue) { assert(weightIndex < 4 && weightIndex >= 0); var weightValues = weights; + // Clear the bits for this weight value. weightValues &= ~(0xFF << (weightIndex * 8)); + // Set the bits for this weight value. weights = weightValues | (weightValue << (weightIndex * 8)); } int _getRawWeight(int weightIndex) => (weights >> (weightIndex * 8)) & 0xFF; + double getWeight(int tendonIndex) { for (int i = 0; i < 4; i++) { if (getTendon(i) == tendonIndex + 1) { @@ -123,5 +151,6 @@ class _WeightHelper { final int index; final int tendon; int weight; + _WeightHelper(this.index, this.tendon, this.weight); } diff --git a/lib/src/rive_core/component.dart b/lib/src/rive_core/component.dart index abe65ef..82e7903 100644 --- a/lib/src/rive_core/component.dart +++ b/lib/src/rive_core/component.dart @@ -2,6 +2,7 @@ import 'package:rive/src/core/core.dart'; import 'package:flutter/foundation.dart'; import 'package:rive/src/rive_core/artboard.dart'; import 'package:rive/src/rive_core/container_component.dart'; + import 'package:rive/src/generated/component_base.dart'; import 'package:rive/src/utilities/dependency_sorter.dart'; import 'package:rive/src/utilities/tops.dart'; @@ -11,20 +12,36 @@ abstract class Component extends ComponentBase implements DependencyGraphNode, Parentable { Artboard? _artboard; dynamic _userData; + + /// Override to true if you want some object inheriting from Component to not + /// have a parent. Most objects will validate that they have a parent during + /// the onAdded callback otherwise they are considered invalid and are culled + /// from core. bool get canBeOrphaned => false; + + // Used during update process. int graphOrder = 0; int dirt = 0xFFFF; + + // This is really only for sanity and earlying out of recursive loops. static const int maxTreeDepth = 5000; + bool addDirt(int value, {bool recurse = false}) { if ((dirt & value) == value) { + // Already marked. return false; } + + // Make sure dirt is set before calling anything that can set more dirt. dirt |= value; + onDirty(dirt); artboard?.onComponentDirty(this); + if (!recurse) { return true; } + for (final d in dependents) { d.addDirt(value, recurse: recurse); } @@ -33,7 +50,12 @@ abstract class Component extends ComponentBase void onDirty(int mask) {} void update(int dirt); + + /// The artboard this component belongs to. Artboard? get artboard => _artboard; + + // Note that this isn't a setter as we don't want anything externally changing + // the artboard. void _changeArtboard(Artboard? value) { if (_artboard == value) { return; @@ -43,8 +65,14 @@ abstract class Component extends ComponentBase _artboard?.addComponent(this); } + /// Called whenever we're resolving the artboard, we piggy back on that + /// process to visit ancestors in the tree. This is a good opportunity to + /// check if we have an ancestor of a specific type. For example, a Path needs + /// to know which Shape it's within. @mustCallSuper void visitAncestor(Component ancestor) {} + + /// Find the artboard in the hierarchy. bool resolveArtboard() { int sanity = maxTreeDepth; for (Component? curr = this; @@ -71,6 +99,7 @@ abstract class Component extends ComponentBase } void userDataChanged(dynamic from, dynamic to) {} + @override void parentIdChanged(int from, int to) { parent = context.resolve(to); @@ -79,6 +108,7 @@ abstract class Component extends ComponentBase ContainerComponent? _parent; @override ContainerComponent? get parent => _parent; + set parent(ContainerComponent? value) { if (_parent == value) { return; @@ -93,28 +123,40 @@ abstract class Component extends ComponentBase void parentChanged(ContainerComponent? from, ContainerComponent? to) { from?.children.remove(this); from?.childRemoved(this); + to?.children.add(this); to?.childAdded(this); + + // We need to resolve our artboard. markRebuildDependencies(); } + /// Components that depend on this component. final Set _dependents = {}; + + /// Components that this component depends on. final Set _dependsOn = {}; + @override Set get dependents => _dependents; + bool addDependent(Component dependent) { assert(artboard == dependent.artboard, 'Components must be in the same artboard.'); + if (!_dependents.add(dependent)) { return false; } dependent._dependsOn.add(this); + return true; } bool isValidParent(Component parent) => parent is ContainerComponent; + void markRebuildDependencies() { if (!context.markDependenciesDirty(this)) { + // no context, or already dirty. return; } for (final dependent in _dependents) { @@ -128,11 +170,18 @@ abstract class Component extends ComponentBase parentDep._dependents.remove(this); } _dependsOn.clear(); + // by default a component depends on nothing (likely it will depend on the + // parent but we leave that for specific implementations to supply). } + /// Something we depend on has been removed. It's important to clear out any + /// stored references to that dependency so it can be garbage collected (if + /// necessary). void onDependencyRemoved(Component dependent) {} + @override void onAdded() {} + @override void onAddedDirty() { if (parentId != Core.missingId) { @@ -140,6 +189,11 @@ abstract class Component extends ComponentBase } } + /// When a component has been removed from the Core Context, we clean up any + /// dangling references left on the parent and on any other dependent + /// component. It's important for specialization of Component to respond to + /// override [onDependencyRemoved] and clean up any further stored references + /// to that component (for example the target of a Constraint). @override @mustCallSuper void onRemoved() { @@ -148,14 +202,22 @@ abstract class Component extends ComponentBase parentDep._dependents.remove(this); } _dependsOn.clear(); + for (final dependent in _dependents) { dependent.onDependencyRemoved(this); } _dependents.clear(); + + // silently clear from the parent in order to not cause any further undo + // stack changes if (parent != null) { parent!.children.remove(this); parent!.childRemoved(this); } + + // The artboard containing this component will need its dependencies + // re-sorted. + if (artboard != null) { context.markDependencyOrderDirty(); _changeArtboard(null); @@ -168,7 +230,10 @@ abstract class Component extends ComponentBase } @override - void nameChanged(String from, String to) {} + void nameChanged(String from, String to) { + /// Changing name doesn't really do anything. + } + @override bool import(ImportStack stack) { var artboardImporter = stack.latest(ArtboardBase.typeKey); @@ -176,6 +241,7 @@ abstract class Component extends ComponentBase return false; } artboardImporter.addComponent(this); + return super.import(stack); } } diff --git a/lib/src/rive_core/component_dirt.dart b/lib/src/rive_core/component_dirt.dart index ddda4d6..f30bd03 100644 --- a/lib/src/rive_core/component_dirt.dart +++ b/lib/src/rive_core/component_dirt.dart @@ -1,14 +1,41 @@ class ComponentDirt { static const int dependents = 1 << 0; + + /// General flag for components are dirty (if this is up, the update cycle + /// runs). It gets automatically applied with any other dirt. static const int components = 1 << 1; + + /// Draw order needs to be re-computed. static const int drawOrder = 1 << 2; + + /// Draw order needs to be re-computed. static const int naturalDrawOrder = 1 << 3; + + /// Path is dirty and needs to be rebuilt. static const int path = 1 << 4; + + /// Vertices have changed, re-order cached lists. static const int vertices = 1 << 5; + + /// Used by any component that needs to recompute their local transform. + /// Usually components that have their transform dirty will also have their + /// worldTransform dirty. static const int transform = 1 << 6; + + /// Used by any component that needs to update its world transform. static const int worldTransform = 1 << 7; + + /// Dirt used to mark some stored paint needs to be rebuilt or that we just + /// want to trigger an update cycle so painting occurs. static const int paint = 1 << 8; + + /// Used by the gradients track when the stops need to be re-ordered. static const int stops = 1 << 9; + + /// Used by ClippingShape to help Shape know when to recalculate its list of + /// clipping sources. static const int clip = 1 << 10; + + /// Set when blend modes need to be updated. static const int blendMode = 1 << 11; } diff --git a/lib/src/rive_core/component_flags.dart b/lib/src/rive_core/component_flags.dart index a999c6b..0b48151 100644 --- a/lib/src/rive_core/component_flags.dart +++ b/lib/src/rive_core/component_flags.dart @@ -1,4 +1,8 @@ class ComponentFlags { + /// Whether the component should be drawn (at runtime this only used by + /// drawables and paths). static const int hidden = 1 << 0; + + // Whether the component was locked for editing in the editor. static const int locked = 1 << 1; } diff --git a/lib/src/rive_core/container_component.dart b/lib/src/rive_core/container_component.dart index 4ab8e17..8d0fb6c 100644 --- a/lib/src/rive_core/container_component.dart +++ b/lib/src/rive_core/container_component.dart @@ -19,7 +19,11 @@ abstract class ContainerComponent extends ContainerComponentBase { @mustCallSuper void childAdded(Component child) {} + void childRemoved(Component child) {} + + // Make sure that the current function can be applied to the current + // [Component], before descending onto all the children. bool forAll(DescentCallback cb) { if (cb(this) == false) { return false; @@ -28,17 +32,27 @@ abstract class ContainerComponent extends ContainerComponentBase { return true; } + // Recursively descend onto all the children in the hierarchy tree. + // If the callback returns false, it won't recurse down a particular branch. void forEachChild(DescentCallback cb) { for (final child in children) { if (cb(child) == false) { continue; } + + // TODO: replace with a more robust check. if (child is ContainerComponent) { child.forEachChild(cb); } } } + /// Recursive version of [Component.remove]. This should only be called when + /// you know this is the only part of the branch you are removing in your + /// operation. If your operation could remove items from the same branch + /// multiple times, you should consider building up a list of the individual + /// items to remove and then remove them individually to avoid calling remove + /// multiple times on children. void removeRecursive() { Set deathRow = {this}; forEachChild((child) => deathRow.add(child)); diff --git a/lib/src/rive_core/draw_rules.dart b/lib/src/rive_core/draw_rules.dart index 7a11973..58ef46d 100644 --- a/lib/src/rive_core/draw_rules.dart +++ b/lib/src/rive_core/draw_rules.dart @@ -7,10 +7,12 @@ export 'package:rive/src/generated/draw_rules_base.dart'; class DrawRules extends DrawRulesBase { final Set _targets = {}; Set get targets => _targets; + DrawTarget? _activeTarget; DrawTarget? get activeTarget => _activeTarget; set activeTarget(DrawTarget? value) => drawTargetId = value?.id ?? Core.missingId; + @override void drawTargetIdChanged(int from, int to) { _activeTarget = context.resolve(to); @@ -25,12 +27,14 @@ class DrawRules extends DrawRulesBase { @override void update(int dirt) {} + @override void childAdded(Component child) { super.childAdded(child); switch (child.coreType) { case DrawTargetBase.typeKey: _targets.add(child as DrawTarget); + break; } } @@ -44,6 +48,7 @@ class DrawRules extends DrawRulesBase { if (_targets.isEmpty) { remove(); } + break; } } diff --git a/lib/src/rive_core/draw_target.dart b/lib/src/rive_core/draw_target.dart index 42dd856..f155339 100644 --- a/lib/src/rive_core/draw_target.dart +++ b/lib/src/rive_core/draw_target.dart @@ -6,14 +6,17 @@ export 'package:rive/src/generated/draw_target_base.dart'; enum DrawTargetPlacement { before, after } class DrawTarget extends DrawTargetBase { + // Store first and last drawables that are affected by this target. Drawable? first; Drawable? last; + Drawable? _drawable; Drawable? get drawable => _drawable; set drawable(Drawable? value) { if (_drawable == value) { return; } + _drawable = value; drawableId = value?.id ?? Core.missingId; } @@ -21,6 +24,7 @@ class DrawTarget extends DrawTargetBase { DrawTargetPlacement get placement => DrawTargetPlacement.values[placementValue]; set placement(DrawTargetPlacement value) => placementValue = value.index; + @override void drawableIdChanged(int from, int to) { drawable = context.resolve(to); diff --git a/lib/src/rive_core/drawable.dart b/lib/src/rive_core/drawable.dart index 60a3d9f..2f15f9f 100644 --- a/lib/src/rive_core/drawable.dart +++ b/lib/src/rive_core/drawable.dart @@ -1,4 +1,5 @@ import 'dart:ui'; + import 'package:rive/src/rive_core/component_dirt.dart'; import 'package:rive/src/rive_core/component_flags.dart'; import 'package:rive/src/rive_core/container_component.dart'; @@ -9,23 +10,37 @@ import 'package:rive/src/rive_core/transform_component.dart'; export 'package:rive/src/generated/drawable_base.dart'; abstract class Drawable extends DrawableBase { + /// Flattened rules inherited from parents (or self) so we don't have to look + /// up the tree when re-sorting. DrawRules? flattenedDrawRules; + + /// The previous drawable in the draw order. Drawable? prev; + + /// The next drawable in the draw order. Drawable? next; + @override void buildDrawOrder( List drawables, DrawRules? rules, List allRules) { flattenedDrawRules = drawRules ?? rules; + drawables.add(this); + super.buildDrawOrder(drawables, rules, allRules); } + /// Draw the contents of this drawable component in world transform space. void draw(Canvas canvas); + BlendMode get blendMode => BlendMode.values[blendModeValue]; set blendMode(BlendMode value) => blendModeValue = value.index; + @override void blendModeValueChanged(int from, int to) {} + List _clippingShapes = []; + bool clip(Canvas canvas) { if (_clippingShapes.isEmpty) { return false; @@ -43,6 +58,8 @@ abstract class Drawable extends DrawableBase { @override void parentChanged(ContainerComponent? from, ContainerComponent? to) { super.parentChanged(from, to); + // Make sure we re-compute clipping shapes when we change parents. Issue + // #1586 addDirt(ComponentDirt.clip); } @@ -50,6 +67,7 @@ abstract class Drawable extends DrawableBase { void update(int dirt) { super.update(dirt); if (dirt & ComponentDirt.clip != 0) { + // Find clip in parents. List clippingShapes = []; for (ContainerComponent? p = this; p != null; p = p.parent) { if (p is TransformComponent) { @@ -62,7 +80,9 @@ abstract class Drawable extends DrawableBase { } } + // When drawable flags change, repaint. @override void drawableFlagsChanged(int from, int to) => addDirt(ComponentDirt.paint); + bool get isHidden => (drawableFlags & ComponentFlags.hidden) != 0; } diff --git a/lib/src/rive_core/event.dart b/lib/src/rive_core/event.dart index a8f87db..d369c86 100644 --- a/lib/src/rive_core/event.dart +++ b/lib/src/rive_core/event.dart @@ -1,6 +1,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +// Just a way to get around the protected notifyListeners so we can use trigger +// multiple events from a single object. class Event extends ChangeNotifier { void notify() => notifyListeners(); } diff --git a/lib/src/rive_core/math/aabb.dart b/lib/src/rive_core/math/aabb.dart index 9a291bf..a26a175 100644 --- a/lib/src/rive_core/math/aabb.dart +++ b/lib/src/rive_core/math/aabb.dart @@ -5,16 +5,19 @@ import 'package:rive/src/rive_core/math/vec2d.dart'; class AABB { Float32List _buffer; + Float32List get values { return _buffer; } Vec2D get topLeft => minimum; + Vec2D get topRight { return Vec2D.fromValues(_buffer[2], _buffer[1]); } Vec2D get bottomRight => maximum; + Vec2D get bottomLeft { return Vec2D.fromValues(_buffer[0], _buffer[3]); } @@ -31,10 +34,14 @@ class AABB { double get maxX => _buffer[2]; double get minY => _buffer[1]; double get maxY => _buffer[3]; + AABB() : _buffer = Float32List.fromList([0.0, 0.0, 0.0, 0.0]); + AABB.clone(AABB a) : _buffer = Float32List.fromList(a.values); + AABB.fromValues(double a, double b, double c, double d) : _buffer = Float32List.fromList([a, b, c, d]); + AABB.empty() : _buffer = Float32List.fromList([ double.maxFinite, @@ -42,6 +49,7 @@ class AABB { -double.maxFinite, -double.maxFinite ]); + factory AABB.expand(AABB from, double amount) { var aabb = AABB.clone(from); if (aabb.width < amount) { @@ -54,6 +62,7 @@ class AABB { } return aabb; } + factory AABB.pad(AABB from, double amount) { var aabb = AABB.clone(from); aabb[0] -= amount; @@ -62,7 +71,9 @@ class AABB { aabb[3] += amount; return aabb; } + bool get isEmpty => !AABB.isValid(this); + Vec2D includePoint(Vec2D point, Mat2D? transform) { var transformedPoint = transform == null ? point @@ -90,12 +101,15 @@ class AABB { AABB.fromMinMax(Vec2D min, Vec2D max) : _buffer = Float32List.fromList([min[0], min[1], max[0], max[1]]); + static bool areEqual(AABB a, AABB b) { return a[0] == b[0] && a[1] == b[1] && a[2] == b[2] && a[3] == b[3]; } double get width => _buffer[2] - _buffer[0]; + double get height => _buffer[3] - _buffer[1]; + double operator [](int idx) { return _buffer[idx]; } @@ -162,14 +176,18 @@ class AABB { static bool testOverlap(AABB a, AABB b) { double d1x = b[0] - a[2]; double d1y = b[1] - a[3]; + double d2x = a[0] - b[2]; double d2y = a[1] - b[3]; + if (d1x > 0.0 || d1y > 0.0) { return false; } + if (d2x > 0.0 || d2y > 0.0) { return false; } + return true; } @@ -181,6 +199,7 @@ class AABB { AABB translate(Vec2D vec) => AABB.fromValues(_buffer[0] + vec[0], _buffer[1] + vec[1], _buffer[2] + vec[0], _buffer[3] + vec[1]); + @override String toString() { return _buffer.toString(); @@ -195,16 +214,23 @@ class AABB { ], transform: matrix); } - factory AABB.fromPoints(Iterable points, - {Mat2D? transform, double expand = 0}) { + /// Compute an AABB from a set of points with an optional [transform] to apply + /// before computing. + factory AABB.fromPoints( + Iterable points, { + Mat2D? transform, + double expand = 0, + }) { double minX = double.maxFinite; double minY = double.maxFinite; double maxX = -double.maxFinite; double maxY = -double.maxFinite; + for (final point in points) { var p = transform == null ? point : Vec2D.transformMat2D(Vec2D(), point, transform); + double x = p[0]; double y = p[1]; if (x < minX) { @@ -213,6 +239,7 @@ class AABB { if (y < minY) { minY = y; } + if (x > maxX) { maxX = x; } @@ -220,6 +247,8 @@ class AABB { maxY = y; } } + + // Make sure the box is at least this wide/high if (expand != 0) { double width = maxX - minX; double diff = expand - width; @@ -230,6 +259,7 @@ class AABB { } double height = maxY - minY; diff = expand - height; + if (diff > 0) { diff /= 2; minY -= diff; diff --git a/lib/src/rive_core/math/circle_constant.dart b/lib/src/rive_core/math/circle_constant.dart index 70e7187..4710375 100644 --- a/lib/src/rive_core/math/circle_constant.dart +++ b/lib/src/rive_core/math/circle_constant.dart @@ -1,2 +1,4 @@ +/// Use this for perfect rounded corners. +/// https://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves const circleConstant = 0.552284749831; const icircleConstant = 1 - circleConstant; diff --git a/lib/src/rive_core/math/mat2d.dart b/lib/src/rive_core/math/mat2d.dart index 24a0753..e49a6ef 100644 --- a/lib/src/rive_core/math/mat2d.dart +++ b/lib/src/rive_core/math/mat2d.dart @@ -1,8 +1,11 @@ import 'dart:math'; import 'dart:typed_data'; + import 'package:rive/src/rive_core/math/transform_components.dart'; import 'package:rive/src/rive_core/math/vec2d.dart'; +/// Can't make this constant so we override and disable changing values so we'll +/// throw if something tries to change the identity. class _Identity extends Mat2D { @override void operator []=(int index, double value) => throw UnsupportedError( @@ -12,6 +15,7 @@ class _Identity extends Mat2D { class Mat2D { static final Mat2D identity = _Identity(); final Float32List _buffer; + Float32List get values { return _buffer; } @@ -46,15 +50,20 @@ class Mat2D { } Mat2D() : _buffer = Float32List.fromList([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]); + Mat2D.fromTranslation(Vec2D translation) : _buffer = Float32List.fromList( [1.0, 0.0, 0.0, 1.0, translation[0], translation[1]]); + Mat2D.fromScaling(Vec2D scaling) : _buffer = Float32List.fromList([scaling[0], 0, 0, scaling[1], 0, 0]); + Mat2D.fromMat4(Float64List mat4) : _buffer = Float32List.fromList( [mat4[0], mat4[1], mat4[4], mat4[5], mat4[12], mat4[13]]); + Mat2D.clone(Mat2D copy) : _buffer = Float32List.fromList(copy.values); + static Mat2D fromRotation(Mat2D o, double rad) { double s = sin(rad); double c = cos(rad); @@ -163,11 +172,13 @@ class Mat2D { static bool invert(Mat2D o, Mat2D a) { double aa = a[0], ab = a[1], ac = a[2], ad = a[3], atx = a[4], aty = a[5]; + double det = aa * ad - ab * ac; if (det == 0.0) { return false; } det = 1.0 / det; + o[0] = ad * det; o[1] = -ab * det; o[2] = -ac * det; @@ -181,6 +192,7 @@ class Mat2D { double x = m[0]; double y = m[1]; s[0] = x.sign * sqrt(x * x + y * y); + x = m[2]; y = m[3]; s[1] = y.sign * sqrt(x * x + y * y); @@ -203,11 +215,13 @@ class Mat2D { static void decompose(Mat2D m, TransformComponents result) { double m0 = m[0], m1 = m[1], m2 = m[2], m3 = m[3]; + double rotation = atan2(m1, m0); double denom = m0 * m0 + m1 * m1; double scaleX = sqrt(denom); double scaleY = (scaleX == 0) ? 0 : ((m0 * m3 - m2 * m1) / scaleX); double skewX = atan2(m0 * m2 + m1 * m3, denom); + result[0] = m[4]; result[1] = m[5]; result[2] = scaleX; @@ -218,6 +232,7 @@ class Mat2D { static void compose(Mat2D m, TransformComponents result) { double r = result[4]; + if (r != 0.0) { Mat2D.fromRotation(m, r); } else { @@ -226,6 +241,7 @@ class Mat2D { m[4] = result[0]; m[5] = result[1]; Mat2D.scale(m, m, result.scale); + double sk = result[5]; if (sk != 0.0) { m[2] = m[0] * sk + m[2]; diff --git a/lib/src/rive_core/math/segment2d.dart b/lib/src/rive_core/math/segment2d.dart index 04d48dd..c44a213 100644 --- a/lib/src/rive_core/math/segment2d.dart +++ b/lib/src/rive_core/math/segment2d.dart @@ -1,18 +1,39 @@ import 'package:rive/src/rive_core/math/vec2d.dart'; +/// Result of projecting a point onto a segment. class ProjectionResult { + /// The distance factor from 0-1 along the segment starting at the + /// [Segment2D.start]. final double t; + + /// The actual 2d point in the same space as [Segment2D.start] and + /// [Segment2D.end]. final Vec2D point; + ProjectionResult(this.t, this.point); } +/// A line segment with a discrete [start] and [end]. class Segment2D { + /// The starting point of this line segment. final Vec2D start; + + /// The ending point of this line segment. final Vec2D end; + + /// Difference from start to end. Nullable so we can compute it only when we + /// need it. Vec2D? diff; + + /// The squared length of this segment. double lengthSquared = 0; + Segment2D(this.start, this.end); + + /// Find where the given [point] lies on this segment. ProjectionResult projectPoint(Vec2D point, {bool clamp = true}) { + // We cache these internally so we can call projectPoint multiple times in + // succession performantly. if (diff == null) { diff = Vec2D.subtract(Vec2D(), start, end); lengthSquared = Vec2D.squaredLength(diff!); @@ -23,7 +44,9 @@ class Segment2D { double t = ((point[0] - start[0]) * (end[0] - start[0]) + (point[1] - start[1]) * (end[1] - start[1])) / lengthSquared; + if (clamp) { + // Clamp at edges. if (t < 0.0) { return ProjectionResult(0, start); } @@ -31,9 +54,13 @@ class Segment2D { return ProjectionResult(1, end); } } + return ProjectionResult( - t, - Vec2D.fromValues(start[0] + t * (end[0] - start[0]), - start[1] + t * (end[1] - start[1]))); + t, + Vec2D.fromValues( + start[0] + t * (end[0] - start[0]), + start[1] + t * (end[1] - start[1]), + ), + ); } } diff --git a/lib/src/rive_core/math/transform_components.dart b/lib/src/rive_core/math/transform_components.dart index 34bc35c..6756270 100644 --- a/lib/src/rive_core/math/transform_components.dart +++ b/lib/src/rive_core/math/transform_components.dart @@ -1,9 +1,11 @@ import 'dart:math'; import 'dart:typed_data'; + import 'package:rive/src/rive_core/math/vec2d.dart'; class TransformComponents { final Float32List _buffer; + Float32List get values { return _buffer; } @@ -18,8 +20,10 @@ class TransformComponents { TransformComponents() : _buffer = Float32List.fromList([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]); + TransformComponents.clone(TransformComponents copy) : _buffer = Float32List.fromList(copy.values); + double get x { return _buffer[0]; } diff --git a/lib/src/rive_core/math/vec2d.dart b/lib/src/rive_core/math/vec2d.dart index ddefa8f..7b5042d 100644 --- a/lib/src/rive_core/math/vec2d.dart +++ b/lib/src/rive_core/math/vec2d.dart @@ -5,6 +5,7 @@ import 'package:rive/src/utilities/utilities.dart'; class Vec2D { final Float32List _buffer; + Float32List get values { return _buffer; } @@ -18,8 +19,11 @@ class Vec2D { } Vec2D() : _buffer = Float32List.fromList([0.0, 0.0]); + Vec2D.clone(Vec2D copy) : _buffer = Float32List.fromList(copy._buffer); + Vec2D.fromValues(double x, double y) : _buffer = Float32List.fromList([x, y]); + static void copy(Vec2D o, Vec2D a) { o[0] = a[0]; o[1] = a[1]; @@ -99,6 +103,7 @@ class Vec2D { static Vec2D negate(Vec2D result, Vec2D a) { result[0] = -1 * a[0]; result[1] = -1 * a[1]; + return result; } @@ -143,9 +148,12 @@ class Vec2D { if (t >= 1) { return Vec2D.squaredDistance(segmentPoint2, pt); } + Vec2D ptOnSeg = Vec2D.fromValues( - segmentPoint1[0] + t * (segmentPoint2[0] - segmentPoint1[0]), - segmentPoint1[1] + t * (segmentPoint2[1] - segmentPoint1[1])); + segmentPoint1[0] + t * (segmentPoint2[0] - segmentPoint1[0]), + segmentPoint1[1] + t * (segmentPoint2[1] - segmentPoint1[1]), + ); + return Vec2D.squaredDistance(ptOnSeg, pt); } @@ -165,6 +173,7 @@ class Vec2D { @override bool operator ==(Object o) => o is Vec2D && _buffer[0] == o[0] && _buffer[1] == o[1]; + @override int get hashCode => szudzik(_buffer[0].hashCode, _buffer[1].hashCode); } diff --git a/lib/src/rive_core/node.dart b/lib/src/rive_core/node.dart index eaf97b2..222e777 100644 --- a/lib/src/rive_core/node.dart +++ b/lib/src/rive_core/node.dart @@ -7,6 +7,8 @@ class _UnknownNode extends Node {} class Node extends NodeBase { static final Node unknown = _UnknownNode(); + + /// Sets the position of the Node set translation(Vec2D pos) { x = pos[0]; y = pos[1]; diff --git a/lib/src/rive_core/rive_animation_controller.dart b/lib/src/rive_core/rive_animation_controller.dart index e64948e..007cf0e 100644 --- a/lib/src/rive_core/rive_animation_controller.dart +++ b/lib/src/rive_core/rive_animation_controller.dart @@ -1,9 +1,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +/// Abstraction for receiving a per frame callback while isPlaying is true to +/// apply animation based on an elapsed amount of time. abstract class RiveAnimationController { final _isActive = ValueNotifier(false); ValueListenable get isActiveChanged => _isActive; + bool get isActive => _isActive.value; set isActive(bool value) { if (_isActive.value != value) { @@ -20,7 +23,11 @@ abstract class RiveAnimationController { void onActivate() {} @protected void onDeactivate() {} + + /// Apply animation to objects registered in [core]. Note that a [core] + /// context is specified as animations can be applied to instances. void apply(T core, double elapsedSeconds); + bool init(T core) => true; void dispose() {} } diff --git a/lib/src/rive_core/runtime/exceptions/rive_format_error_exception.dart b/lib/src/rive_core/runtime/exceptions/rive_format_error_exception.dart index efda20a..e90bb00 100644 --- a/lib/src/rive_core/runtime/exceptions/rive_format_error_exception.dart +++ b/lib/src/rive_core/runtime/exceptions/rive_format_error_exception.dart @@ -1,5 +1,6 @@ import 'package:meta/meta.dart'; +/// Thrown when a file being read doesn't match the Rive format. @immutable class RiveFormatErrorException implements Exception { final String cause; diff --git a/lib/src/rive_core/runtime/exceptions/rive_unsupported_version_exception.dart b/lib/src/rive_core/runtime/exceptions/rive_unsupported_version_exception.dart index 52f6c7d..b505940 100644 --- a/lib/src/rive_core/runtime/exceptions/rive_unsupported_version_exception.dart +++ b/lib/src/rive_core/runtime/exceptions/rive_unsupported_version_exception.dart @@ -1,5 +1,7 @@ import 'package:meta/meta.dart'; +/// Error that occurs when a file being loaded doesn't match the importer's +/// supported version. @immutable class RiveUnsupportedVersionException implements Exception { final int majorVersion; @@ -8,6 +10,7 @@ class RiveUnsupportedVersionException implements Exception { final int fileMinorVersion; const RiveUnsupportedVersionException(this.majorVersion, this.minorVersion, this.fileMajorVersion, this.fileMinorVersion); + @override String toString() { return 'File contains version $fileMajorVersion.$fileMinorVersion. ' diff --git a/lib/src/rive_core/runtime/runtime_header.dart b/lib/src/rive_core/runtime/runtime_header.dart index b5c6565..acf01c1 100644 --- a/lib/src/rive_core/runtime/runtime_header.dart +++ b/lib/src/rive_core/runtime/runtime_header.dart @@ -1,11 +1,15 @@ import 'dart:collection'; + import 'package:rive/src/rive_core/runtime/exceptions/rive_format_error_exception.dart'; import 'package:rive/src/rive_core/runtime/exceptions/rive_unsupported_version_exception.dart'; import 'package:rive/src/utilities/binary_buffer/binary_reader.dart'; +/// Stores the minor and major version of Rive. Versions with the same major +/// value are backwards and forwards compatible. class RuntimeVersion { final int major; final int minor; + const RuntimeVersion(this.major, this.minor); String versionString() { return '$major.$minor'; @@ -17,19 +21,26 @@ const riveVersion = RuntimeVersion(7, 0); class RuntimeHeader { static const String fingerprint = 'RIVE'; final RuntimeVersion version; + final int fileId; + final HashMap propertyToFieldIndex; - RuntimeHeader( - {required this.fileId, - required this.version, - required this.propertyToFieldIndex}); + + RuntimeHeader({ + required this.fileId, + required this.version, + required this.propertyToFieldIndex, + }); + factory RuntimeHeader.read(BinaryReader reader) { var fingerprint = RuntimeHeader.fingerprint.codeUnits; + for (int i = 0; i < fingerprint.length; i++) { if (reader.readUint8() != fingerprint[i]) { throw const RiveFormatErrorException('Fingerprint doesn\'t match.'); } } + int readMajorVersion = reader.readVarUint(); int readMinorVersion = reader.readVarUint(); if (readMajorVersion > riveVersion.major) { @@ -40,7 +51,9 @@ class RuntimeHeader { reader.readVarUint(); } int fileId = reader.readVarUint(); + var propertyFields = HashMap(); + var propertyKeys = []; for (int propertyKey = reader.readVarUint(); propertyKey != 0; @@ -58,9 +71,11 @@ class RuntimeHeader { propertyFields[propertyKey] = fieldIndex; currentBit += 2; } + return RuntimeHeader( - fileId: fileId, - version: RuntimeVersion(readMajorVersion, readMinorVersion), - propertyToFieldIndex: propertyFields); + fileId: fileId, + version: RuntimeVersion(readMajorVersion, readMinorVersion), + propertyToFieldIndex: propertyFields, + ); } } diff --git a/lib/src/rive_core/shapes/clipping_shape.dart b/lib/src/rive_core/shapes/clipping_shape.dart index f8258cd..ef0da44 100644 --- a/lib/src/rive_core/shapes/clipping_shape.dart +++ b/lib/src/rive_core/shapes/clipping_shape.dart @@ -1,4 +1,5 @@ import 'dart:ui'; + import 'package:rive/src/rive_core/component_dirt.dart'; import 'package:rive/src/rive_core/node.dart'; import 'package:rive/src/rive_core/shapes/shape.dart'; @@ -10,19 +11,25 @@ class ClippingShape extends ClippingShapeBase { final List _shapes = []; PathFillType get fillType => PathFillType.values[fillRule]; set fillType(PathFillType type) => fillRule = type.index; + Node _source = Node.unknown; Node get source => _source; set source(Node value) { if (_source == value) { return; } + _source = value; sourceId = value.id; } @override void fillRuleChanged(int from, int to) { + // In the future, if clipOp can change at runtime (animation), we may want + // the shapes that use this as a clipping source to make them depend on this + // clipping shape so we can add dirt to them directly. parent?.addDirt(ComponentDirt.clip, recurse: true); + addDirt(ComponentDirt.path); } @@ -44,10 +51,13 @@ class ClippingShape extends ClippingShapeBase { _source.forAll((component) { if (component is Shape) { _shapes.add(component); + //component.addDependent(this); component.pathComposer.addDependent(this); } return true; }); + + // make sure we rebuild the clipping path. addDirt(ComponentDirt.path); } @@ -60,6 +70,8 @@ class ClippingShape extends ClippingShapeBase { @override void update(int dirt) { if (dirt & (ComponentDirt.worldTransform | ComponentDirt.path) != 0) { + // Build the clipping path as one of our dependent shapes changes or we + // added a shape. clippingPath.reset(); clippingPath.fillType = fillType; for (final shape in _shapes) { @@ -75,6 +87,7 @@ class ClippingShape extends ClippingShapeBase { @override void isVisibleChanged(bool from, bool to) { + // Redraw _source.addDirt(ComponentDirt.paint); } } diff --git a/lib/src/rive_core/shapes/cubic_asymmetric_vertex.dart b/lib/src/rive_core/shapes/cubic_asymmetric_vertex.dart index fec3ba2..d90304a 100644 --- a/lib/src/rive_core/shapes/cubic_asymmetric_vertex.dart +++ b/lib/src/rive_core/shapes/cubic_asymmetric_vertex.dart @@ -1,4 +1,5 @@ import 'dart:math'; + import 'package:rive/src/core/core.dart'; import 'package:rive/src/rive_core/component_dirt.dart'; import 'package:rive/src/rive_core/math/vec2d.dart'; @@ -10,8 +11,10 @@ class CubicAsymmetricVertex extends CubicAsymmetricVertexBase { CubicAsymmetricVertex.procedural() { InternalCoreHelper.markValid(this); } + Vec2D? _inPoint; Vec2D? _outPoint; + @override Vec2D get outPoint { return _outPoint ??= Vec2D.add( diff --git a/lib/src/rive_core/shapes/cubic_detached_vertex.dart b/lib/src/rive_core/shapes/cubic_detached_vertex.dart index 7566bac..8e8d4c0 100644 --- a/lib/src/rive_core/shapes/cubic_detached_vertex.dart +++ b/lib/src/rive_core/shapes/cubic_detached_vertex.dart @@ -1,4 +1,5 @@ import 'dart:math'; + import 'package:rive/src/core/core.dart'; import 'package:rive/src/rive_core/component_dirt.dart'; import 'package:rive/src/rive_core/math/vec2d.dart'; @@ -8,16 +9,18 @@ export 'package:rive/src/generated/shapes/cubic_detached_vertex_base.dart'; class CubicDetachedVertex extends CubicDetachedVertexBase { Vec2D? _inPoint; Vec2D? _outPoint; + CubicDetachedVertex(); - CubicDetachedVertex.fromValues( - {required double x, - required double y, - double? inX, - double? inY, - double? outX, - double? outY, - Vec2D? inPoint, - Vec2D? outPoint}) { + CubicDetachedVertex.fromValues({ + required double x, + required double y, + double? inX, + double? inY, + double? outX, + double? outY, + Vec2D? inPoint, + Vec2D? outPoint, + }) { InternalCoreHelper.markValid(this); this.x = x; this.y = y; @@ -25,12 +28,14 @@ class CubicDetachedVertex extends CubicDetachedVertexBase { this.outPoint = Vec2D.fromValues(outX ?? outPoint![0], outY ?? outPoint![1]); } + @override Vec2D get outPoint => _outPoint ??= Vec2D.add( Vec2D(), translation, Vec2D.fromValues( cos(outRotation) * outDistance, sin(outRotation) * outDistance)); + @override set outPoint(Vec2D value) { _outPoint = Vec2D.clone(value); @@ -42,6 +47,7 @@ class CubicDetachedVertex extends CubicDetachedVertexBase { translation, Vec2D.fromValues( cos(inRotation) * inDistance, sin(inRotation) * inDistance)); + @override set inPoint(Vec2D value) { _inPoint = Vec2D.clone(value); diff --git a/lib/src/rive_core/shapes/cubic_mirrored_vertex.dart b/lib/src/rive_core/shapes/cubic_mirrored_vertex.dart index e6aa9bc..31f5514 100644 --- a/lib/src/rive_core/shapes/cubic_mirrored_vertex.dart +++ b/lib/src/rive_core/shapes/cubic_mirrored_vertex.dart @@ -1,4 +1,5 @@ import 'dart:math'; + import 'package:rive/src/core/core.dart'; import 'package:rive/src/rive_core/component_dirt.dart'; import 'package:rive/src/rive_core/math/vec2d.dart'; @@ -7,11 +8,15 @@ export 'package:rive/src/generated/shapes/cubic_mirrored_vertex_base.dart'; class CubicMirroredVertex extends CubicMirroredVertexBase { CubicMirroredVertex(); + + /// Makes a vertex that is disconnected from core. CubicMirroredVertex.procedural() { InternalCoreHelper.markValid(this); } + Vec2D? _inPoint; Vec2D? _outPoint; + @override Vec2D get outPoint { return _outPoint ??= Vec2D.add(Vec2D(), translation, diff --git a/lib/src/rive_core/shapes/cubic_vertex.dart b/lib/src/rive_core/shapes/cubic_vertex.dart index f97e437..25d1bb8 100644 --- a/lib/src/rive_core/shapes/cubic_vertex.dart +++ b/lib/src/rive_core/shapes/cubic_vertex.dart @@ -1,4 +1,5 @@ import 'dart:typed_data'; + import 'package:rive/src/rive_core/bones/weight.dart'; import 'package:rive/src/rive_core/math/mat2d.dart'; import 'package:rive/src/rive_core/math/vec2d.dart'; @@ -8,15 +9,20 @@ export 'package:rive/src/generated/shapes/cubic_vertex_base.dart'; abstract class CubicVertex extends CubicVertexBase { Vec2D get outPoint; Vec2D get inPoint; + set outPoint(Vec2D value); set inPoint(Vec2D value); + @override Vec2D get renderTranslation => weight?.translation ?? super.renderTranslation; + Vec2D get renderIn => weight?.inTranslation ?? inPoint; Vec2D get renderOut => weight?.outTranslation ?? outPoint; + @override void deform(Mat2D world, Float32List boneTransforms) { super.deform(world, boneTransforms); + Weight.deform(outPoint[0], outPoint[1], weight!.outIndices, weight!.outValues, world, boneTransforms, weight!.outTranslation); Weight.deform(inPoint[0], inPoint[1], weight!.inIndices, weight!.inValues, diff --git a/lib/src/rive_core/shapes/ellipse.dart b/lib/src/rive_core/shapes/ellipse.dart index 0f76b63..3afdfc7 100644 --- a/lib/src/rive_core/shapes/ellipse.dart +++ b/lib/src/rive_core/shapes/ellipse.dart @@ -2,6 +2,7 @@ import 'package:rive/src/rive_core/math/circle_constant.dart'; import 'package:rive/src/rive_core/shapes/cubic_detached_vertex.dart'; import 'package:rive/src/rive_core/shapes/path_vertex.dart'; import 'package:rive/src/generated/shapes/ellipse_base.dart'; + export 'package:rive/src/generated/shapes/ellipse_base.dart'; class Ellipse extends EllipseBase { @@ -9,35 +10,40 @@ class Ellipse extends EllipseBase { List get vertices { double ox = -originX * width + radiusX; double oy = -originY * height + radiusY; + return [ CubicDetachedVertex.fromValues( - x: ox, - y: oy - radiusY, - inX: ox - radiusX * circleConstant, - inY: oy - radiusY, - outX: ox + radiusX * circleConstant, - outY: oy - radiusY), + x: ox, + y: oy - radiusY, + inX: ox - radiusX * circleConstant, + inY: oy - radiusY, + outX: ox + radiusX * circleConstant, + outY: oy - radiusY, + ), CubicDetachedVertex.fromValues( - x: ox + radiusX, - y: oy, - inX: ox + radiusX, - inY: oy + circleConstant * -radiusY, - outX: ox + radiusX, - outY: oy + circleConstant * radiusY), + x: ox + radiusX, + y: oy, + inX: ox + radiusX, + inY: oy + circleConstant * -radiusY, + outX: ox + radiusX, + outY: oy + circleConstant * radiusY, + ), CubicDetachedVertex.fromValues( - x: ox, - y: oy + radiusY, - inX: ox + radiusX * circleConstant, - inY: oy + radiusY, - outX: ox - radiusX * circleConstant, - outY: oy + radiusY), + x: ox, + y: oy + radiusY, + inX: ox + radiusX * circleConstant, + inY: oy + radiusY, + outX: ox - radiusX * circleConstant, + outY: oy + radiusY, + ), CubicDetachedVertex.fromValues( - x: ox - radiusX, - y: oy, - inX: ox - radiusX, - inY: oy + radiusY * circleConstant, - outX: ox - radiusX, - outY: oy - radiusY * circleConstant) + x: ox - radiusX, + y: oy, + inX: ox - radiusX, + inY: oy + radiusY * circleConstant, + outX: ox - radiusX, + outY: oy - radiusY * circleConstant, + ), ]; } diff --git a/lib/src/rive_core/shapes/paint/fill.dart b/lib/src/rive_core/shapes/paint/fill.dart index 410a3ca..c65f581 100644 --- a/lib/src/rive_core/shapes/paint/fill.dart +++ b/lib/src/rive_core/shapes/paint/fill.dart @@ -1,19 +1,28 @@ import 'dart:ui'; + import 'package:rive/src/rive_core/component_dirt.dart'; import 'package:rive/src/rive_core/shapes/shape_paint_container.dart'; import 'package:rive/src/generated/shapes/paint/fill_base.dart'; export 'package:rive/src/generated/shapes/paint/fill_base.dart'; +/// A fill Shape painter. class Fill extends FillBase { @override Paint makePaint() => Paint()..style = PaintingStyle.fill; + PathFillType get fillType => PathFillType.values[fillRule]; set fillType(PathFillType type) => fillRule = type.index; + @override void fillRuleChanged(int from, int to) => parent?.addDirt(ComponentDirt.paint); + @override - void update(int dirt) {} + void update(int dirt) { + // Intentionally empty, fill doesn't update. + // Because Fill never adds dependencies, it'll also never get called. + } + @override void onAdded() { super.onAdded(); diff --git a/lib/src/rive_core/shapes/paint/gradient_stop.dart b/lib/src/rive_core/shapes/paint/gradient_stop.dart index 7bb440c..821c32b 100644 --- a/lib/src/rive_core/shapes/paint/gradient_stop.dart +++ b/lib/src/rive_core/shapes/paint/gradient_stop.dart @@ -1,7 +1,9 @@ import 'dart:ui' as ui; + import 'package:rive/src/rive_core/container_component.dart'; import 'package:rive/src/generated/shapes/paint/gradient_stop_base.dart'; import 'package:rive/src/rive_core/shapes/paint/linear_gradient.dart'; + export 'package:rive/src/generated/shapes/paint/gradient_stop_base.dart'; class GradientStop extends GradientStopBase { @@ -24,8 +26,10 @@ class GradientStop extends GradientStopBase { @override void update(int dirt) {} + @override bool validate() => super.validate() && _gradient != null; + @override void parentChanged(ContainerComponent? from, ContainerComponent? to) { super.parentChanged(from, to); diff --git a/lib/src/rive_core/shapes/paint/linear_gradient.dart b/lib/src/rive_core/shapes/paint/linear_gradient.dart index c2accbf..b4102c1 100644 --- a/lib/src/rive_core/shapes/paint/linear_gradient.dart +++ b/lib/src/rive_core/shapes/paint/linear_gradient.dart @@ -1,15 +1,24 @@ import 'dart:ui' as ui; + import 'package:meta/meta.dart'; import 'package:rive/src/rive_core/component.dart'; import 'package:rive/src/rive_core/component_dirt.dart'; import 'package:rive/src/rive_core/math/vec2d.dart'; import 'package:rive/src/rive_core/shapes/paint/gradient_stop.dart'; import 'package:rive/src/rive_core/shapes/paint/shape_paint_mutator.dart'; +import 'package:rive/src/rive_core/shapes/shape.dart'; import 'package:rive/src/generated/shapes/paint/linear_gradient_base.dart'; export 'package:rive/src/generated/shapes/paint/linear_gradient_base.dart'; +/// A core linear gradient. Can be added as a child to a [Shape]'s [Fill] or +/// [Stroke] to paint that Fill or Stroke with a gradient. This is the +/// foundation for the RadialGradient which is very similar but also has a +/// radius value. class LinearGradient extends LinearGradientBase with ShapePaintMutator { + /// Stored list of core gradient stops are in the hierarchy as children of + /// this container. final List gradientStops = []; + bool _paintsInWorldSpace = true; bool get paintsInWorldSpace => _paintsInWorldSpace; set paintsInWorldSpace(bool value) { @@ -22,8 +31,11 @@ class LinearGradient extends LinearGradientBase with ShapePaintMutator { Vec2D get start => Vec2D.fromValues(startX, startY); Vec2D get end => Vec2D.fromValues(endX, endY); + ui.Offset get startOffset => ui.Offset(startX, startY); ui.Offset get endOffset => ui.Offset(endX, endY); + + /// Gradients depends on their shape. @override void buildDependencies() { super.buildDependencies(); @@ -35,6 +47,7 @@ class LinearGradient extends LinearGradientBase with ShapePaintMutator { super.childAdded(child); if (child is GradientStop && !gradientStops.contains(child)) { gradientStops.add(child); + markStopsDirty(); } } @@ -44,31 +57,48 @@ class LinearGradient extends LinearGradientBase with ShapePaintMutator { super.childRemoved(child); if (child is GradientStop && gradientStops.contains(child)) { gradientStops.remove(child); + markStopsDirty(); } } + /// Mark the gradient stops as changed. This will re-sort the stops and + /// rebuild the necessary gradients in the next update cycle. void markStopsDirty() => addDirt(ComponentDirt.stops | ComponentDirt.paint); + + /// Mark the gradient as needing to be rebuilt. This is a more efficient + /// version of markStopsDirty as it won't re-sort the stops. void markGradientDirty() => addDirt(ComponentDirt.paint); + @override void update(int dirt) { + // Do the stops need to be re-ordered? bool stopsChanged = dirt & ComponentDirt.stops != 0; if (stopsChanged) { gradientStops.sort((a, b) => a.position.compareTo(b.position)); } + bool worldTransformed = dirt & ComponentDirt.worldTransform != 0; bool localTransformed = dirt & ComponentDirt.transform != 0; + + // We rebuild the gradient if the gradient is dirty or we paint in world + // space and the world space transform has changed, or the local transform + // has changed. Local transform changes when a stop moves in local space. var rebuildGradient = dirt & ComponentDirt.paint != 0 || localTransformed || (paintsInWorldSpace && worldTransformed); if (rebuildGradient) { + // build up the color and positions lists var colors = []; var colorPositions = []; for (final stop in gradientStops) { colors.add(stop.color); colorPositions.add(stop.position); } + // Check if we need to update the world space gradient. if (paintsInWorldSpace) { + // Get the start and end of the gradient in world coordinates (world + // transform of the shape). var world = shapePaintContainer!.worldTransform; var worldStart = Vec2D.transformMat2D(Vec2D(), start, world); var worldEnd = Vec2D.transformMat2D(Vec2D(), end, world); @@ -85,6 +115,7 @@ class LinearGradient extends LinearGradientBase with ShapePaintMutator { ui.Gradient makeGradient(ui.Offset start, ui.Offset end, List colors, List colorPositions) => ui.Gradient.linear(start, end, colors, colorPositions); + @override void startXChanged(double from, double to) { addDirt(ComponentDirt.transform); @@ -114,6 +145,8 @@ class LinearGradient extends LinearGradientBase with ShapePaintMutator { @override void opacityChanged(double from, double to) { syncColor(); + // We don't need to rebuild anything, just let our shape know we should + // repaint. shapePaintContainer!.addDirt(ComponentDirt.paint); } diff --git a/lib/src/rive_core/shapes/paint/radial_gradient.dart b/lib/src/rive_core/shapes/paint/radial_gradient.dart index d405a3a..1c5baf4 100644 --- a/lib/src/rive_core/shapes/paint/radial_gradient.dart +++ b/lib/src/rive_core/shapes/paint/radial_gradient.dart @@ -3,6 +3,8 @@ import 'package:rive/src/generated/shapes/paint/radial_gradient_base.dart'; export 'package:rive/src/generated/shapes/paint/radial_gradient_base.dart'; class RadialGradient extends RadialGradientBase { + /// We override the make gradient operation to create a radial gradient + /// instead of a linear one. @override ui.Gradient makeGradient(ui.Offset start, ui.Offset end, List colors, List colorPositions) => diff --git a/lib/src/rive_core/shapes/paint/shape_paint.dart b/lib/src/rive_core/shapes/paint/shape_paint.dart index c5b0ea4..4c89b7a 100644 --- a/lib/src/rive_core/shapes/paint/shape_paint.dart +++ b/lib/src/rive_core/shapes/paint/shape_paint.dart @@ -1,34 +1,48 @@ import 'dart:ui'; + import 'package:meta/meta.dart'; import 'package:rive/src/rive_core/component.dart'; import 'package:rive/src/rive_core/component_dirt.dart'; import 'package:rive/src/rive_core/container_component.dart'; import 'package:rive/src/rive_core/shapes/paint/shape_paint_mutator.dart'; +import 'package:rive/src/rive_core/shapes/shape.dart'; import 'package:rive/src/rive_core/shapes/shape_paint_container.dart'; import 'package:rive/src/generated/shapes/paint/shape_paint_base.dart'; export 'package:rive/src/generated/shapes/paint/shape_paint_base.dart'; +/// Generic ShapePaint that abstracts Stroke and Fill. Automatically hooks up +/// parent [Shape] to child [ShapePaintMutator]s. abstract class ShapePaint extends ShapePaintBase { late Paint _paint; Paint get paint => _paint; ShapePaintMutator? _paintMutator; ShapePaintContainer? get shapePaintContainer => parent is ShapePaintContainer ? parent as ShapePaintContainer : null; + ShapePaint() { _paint = makePaint(); } + BlendMode get blendMode => _paint.blendMode; set blendMode(BlendMode value) => _paint.blendMode = value; + double get renderOpacity => _paintMutator!.renderOpacity; set renderOpacity(double value) => _paintMutator!.renderOpacity = value; + ShapePaintMutator? get paintMutator => _paintMutator; + void _changeMutator(ShapePaintMutator? mutator) { _paint = makePaint(); _paintMutator = mutator; } + /// Implementing classes are expected to override this to create a paint + /// object. This gets called whenever the mutator is changed in order to not + /// require each mutator to manually reset the paint to some canonical state. + /// Instead, we simply blow out the old one and make a new one. @protected Paint makePaint(); + @override void childAdded(Component child) { super.childAdded(child); @@ -53,6 +67,7 @@ abstract class ShapePaint extends ShapePaintBase { super.validate() && parent is ShapePaintContainer && _paintMutator != null; + @override void isVisibleChanged(bool from, bool to) { shapePaintContainer?.addDirt(ComponentDirt.paint); @@ -61,6 +76,8 @@ abstract class ShapePaint extends ShapePaintBase { @override void childRemoved(Component child) { super.childRemoved(child); + // Make sure to clean up any references so that they can be garbage + // collected. if (child is ShapePaintMutator && _paintMutator == child as ShapePaintMutator) { _changeMutator(null); @@ -69,5 +86,6 @@ abstract class ShapePaint extends ShapePaintBase { void _initMutator() => _paintMutator?.initializePaintMutator(shapePaintContainer!, paint); + void draw(Canvas canvas, Path path); } diff --git a/lib/src/rive_core/shapes/paint/shape_paint_mutator.dart b/lib/src/rive_core/shapes/paint/shape_paint_mutator.dart index 53544ca..36a6317 100644 --- a/lib/src/rive_core/shapes/paint/shape_paint_mutator.dart +++ b/lib/src/rive_core/shapes/paint/shape_paint_mutator.dart @@ -1,12 +1,17 @@ import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:rive/src/rive_core/shapes/shape_paint_container.dart'; abstract class ShapePaintMutator { ShapePaintContainer? _shapePaintContainer; Paint _paint = Paint(); + + /// The container is usually either a Shape or an Artboard, basically any of + /// the various ContainerComponents that can contain Fills or Strokes. ShapePaintContainer? get shapePaintContainer => _shapePaintContainer; Paint get paint => _paint; + double _renderOpacity = 1; double get renderOpacity => _renderOpacity; set renderOpacity(double value) { @@ -18,6 +23,7 @@ abstract class ShapePaintMutator { @protected void syncColor(); + @mustCallSuper void initializePaintMutator(ShapePaintContainer container, Paint paint) { _shapePaintContainer = container; diff --git a/lib/src/rive_core/shapes/paint/solid_color.dart b/lib/src/rive_core/shapes/paint/solid_color.dart index 6ed8aac..cbe9b34 100644 --- a/lib/src/rive_core/shapes/paint/solid_color.dart +++ b/lib/src/rive_core/shapes/paint/solid_color.dart @@ -1,10 +1,12 @@ import 'dart:ui'; + import 'package:rive/src/rive_core/component_dirt.dart'; import 'package:rive/src/rive_core/shapes/paint/shape_paint.dart'; import 'package:rive/src/rive_core/shapes/paint/shape_paint_mutator.dart'; import 'package:rive/src/generated/shapes/paint/solid_color_base.dart'; export 'package:rive/src/generated/shapes/paint/solid_color_base.dart'; +/// A solid color painter for a shape. Works for both Fill and Stroke. class SolidColor extends SolidColorBase with ShapePaintMutator { Color get color => Color(colorValue); set color(Color c) { @@ -13,12 +15,24 @@ class SolidColor extends SolidColorBase with ShapePaintMutator { @override void colorValueChanged(int from, int to) { + // Since all we need to do is set the color on the paint, we can just do + // this whenever it changes as it's such a lightweight operation. We don't + // need to schedule it for the next update cycle, which saves us from adding + // SolidColor to the dependencies graph. syncColor(); + + // Since we're not in the dependency tree, chuck dirt onto the shape, which + // is. This just ensures we'll paint as soon as possible to show the updated + // color. shapePaintContainer?.addDirt(ComponentDirt.paint); } @override - void update(int dirt) {} + void update(int dirt) { + // Intentionally empty. SolidColor doesn't need an update cycle and doesn't + // depend on anything. + } + @override void syncColor() { paint.color = color @@ -27,6 +41,7 @@ class SolidColor extends SolidColorBase with ShapePaintMutator { @override bool validate() => super.validate() && parent is ShapePaint; + @override void onAdded() { super.onAdded(); diff --git a/lib/src/rive_core/shapes/paint/stroke.dart b/lib/src/rive_core/shapes/paint/stroke.dart index ef78ccd..6382f52 100644 --- a/lib/src/rive_core/shapes/paint/stroke.dart +++ b/lib/src/rive_core/shapes/paint/stroke.dart @@ -6,9 +6,12 @@ import 'package:rive/src/rive_core/shapes/shape_paint_container.dart'; import 'package:rive/src/generated/shapes/paint/stroke_base.dart'; export 'package:rive/src/generated/shapes/paint/stroke_base.dart'; +/// A stroke Shape painter. class Stroke extends StrokeBase { StrokeEffect? _effect; StrokeEffect? get effect => _effect; + + // Should be @internal when supported. // ignore: use_setters_to_change_properties void addStrokeEffect(StrokeEffect effect) { _effect = effect; @@ -26,10 +29,13 @@ class Stroke extends StrokeBase { ..strokeCap = strokeCap ..strokeJoin = strokeJoin ..strokeWidth = thickness; + StrokeCap get strokeCap => StrokeCap.values[cap]; set strokeCap(StrokeCap value) => cap = value.index; + StrokeJoin get strokeJoin => StrokeJoin.values[join]; set strokeJoin(StrokeJoin value) => join = value.index; + @override void capChanged(int from, int to) { paint.strokeCap = StrokeCap.values[to]; @@ -57,7 +63,11 @@ class Stroke extends StrokeBase { } @override - void update(int dirt) {} + void update(int dirt) { + // Intentionally empty, fill doesn't update. + // Because Fill never adds dependencies, it'll also never get called. + } + @override void onAdded() { super.onAdded(); @@ -67,11 +77,13 @@ class Stroke extends StrokeBase { } void invalidateEffects() => _effect?.invalidateEffect(); + @override void draw(Canvas canvas, Path path) { if (!isVisible) { return; } + canvas.drawPath(_effect?.effectPath(path) ?? path, paint); } } diff --git a/lib/src/rive_core/shapes/paint/trim_path.dart b/lib/src/rive_core/shapes/paint/trim_path.dart index bca13f7..a97115e 100644 --- a/lib/src/rive_core/shapes/paint/trim_path.dart +++ b/lib/src/rive_core/shapes/paint/trim_path.dart @@ -1,12 +1,18 @@ import 'dart:ui'; + import 'package:rive/src/rive_core/component_dirt.dart'; import 'package:rive/src/rive_core/shapes/paint/stroke.dart'; import 'package:rive/src/rive_core/shapes/paint/stroke_effect.dart'; import 'package:rive/src/rive_core/shapes/paint/trim_path_drawing.dart'; + import 'package:rive/src/generated/shapes/paint/trim_path_base.dart'; export 'package:rive/src/generated/shapes/paint/trim_path_base.dart'; -enum TrimPathMode { none, sequential, synchronized } +enum TrimPathMode { + none, + sequential, + synchronized, +} class TrimPath extends TrimPathBase implements StrokeEffect { final Path _trimmedPath = Path(); @@ -20,10 +26,12 @@ class TrimPath extends TrimPathBase implements StrokeEffect { var isSequential = mode == TrimPathMode.sequential; double renderStart = start.clamp(0, 1).toDouble(); double renderEnd = end.clamp(0, 1).toDouble(); + bool inverted = renderStart > renderEnd; if ((renderStart - renderEnd).abs() != 1.0) { renderStart = (renderStart + offset) % 1.0; renderEnd = (renderEnd + offset) % 1.0; + if (renderStart < 0) { renderStart += 1.0; } @@ -49,8 +57,10 @@ class TrimPath extends TrimPathBase implements StrokeEffect { } Stroke? get stroke => parent as Stroke?; + TrimPathMode get mode => TrimPathMode.values[modeValue]; set mode(TrimPathMode value) => modeValue = value.index; + @override void invalidateEffect() { _renderPath = null; @@ -59,14 +69,19 @@ class TrimPath extends TrimPathBase implements StrokeEffect { @override void endChanged(double from, double to) => invalidateEffect(); + @override void modeValueChanged(int from, int to) => invalidateEffect(); + @override void offsetChanged(double from, double to) => invalidateEffect(); + @override void startChanged(double from, double to) => invalidateEffect(); + @override void update(int dirt) {} + @override void onAdded() { super.onAdded(); @@ -77,6 +92,7 @@ class TrimPath extends TrimPathBase implements StrokeEffect { @override void onRemoved() { stroke?.removeStrokeEffect(this); + super.onRemoved(); } } diff --git a/lib/src/rive_core/shapes/paint/trim_path_drawing.dart b/lib/src/rive_core/shapes/paint/trim_path_drawing.dart index eb08e56..5baf13b 100644 --- a/lib/src/rive_core/shapes/paint/trim_path_drawing.dart +++ b/lib/src/rive_core/shapes/paint/trim_path_drawing.dart @@ -8,18 +8,27 @@ class _FirstExtractedPath { _FirstExtractedPath(this.path, this.metric, this.length); } +// Returns the path it last extracted from (actually the metrics for that path). _FirstExtractedPath? _appendPathSegmentSequential( - Iterable metrics, Path result, double start, double stop, - {_FirstExtractedPath? first}) { + Iterable metrics, + Path result, + double start, + double stop, { + _FirstExtractedPath? first, +}) { double nextOffset = 0; double offset = 0; for (final metric in metrics) { nextOffset += metric.length; if (start < nextOffset) { + // Store the last metric extracted from so next ops can use it. var st = max(0.0, start - offset); var et = min(metric.length, stop - offset); var extractLength = et - st; Path extracted = metric.extractPath(st, et); + + // If we're re-extracting from the first path, make it look + // contiguous. if (first == null) { // ignore: parameter_assignments first = _FirstExtractedPath(extracted, metric, extractLength); @@ -31,11 +40,13 @@ _FirstExtractedPath? _appendPathSegmentSequential( result.addPath(extracted, Offset.zero); } } else { + // If we extracted this whole sub-path, close it. if (metric.isClosed && extractLength == metric.length) { extracted.close(); } result.addPath(extracted, Offset.zero); } + if (stop < nextOffset) { break; } @@ -46,11 +57,16 @@ _FirstExtractedPath? _appendPathSegmentSequential( } void _appendPathSegmentSync( - PathMetric metric, Path to, double start, double stop, - {bool startWithMoveTo = true}) { + PathMetric metric, + Path to, + double start, + double stop, { + bool startWithMoveTo = true, +}) { double nextOffset = metric.length; if (start < nextOffset) { Path extracted = metric.extractPath(start, stop); + if (startWithMoveTo) { to.addPath(extracted, Offset.zero); } else { @@ -61,11 +77,13 @@ void _appendPathSegmentSync( void _trimPathSequential( Path path, Path result, double startT, double stopT, bool complement) { + // Measure length of all the contours. var metrics = path.computeMetrics().toList(growable: false); double totalLength = 0.0; for (final metric in metrics) { totalLength += metric.length; } + double trimStart = totalLength * startT; double trimStop = totalLength * stopT; _FirstExtractedPath? first; @@ -79,7 +97,12 @@ void _trimPathSequential( first: first); } } else if (trimStart < trimStop) { - first = _appendPathSegmentSequential(metrics, result, trimStart, trimStop); + first = _appendPathSegmentSequential( + metrics, + result, + trimStart, + trimStop, + ); } if (first != null) { if (first.length == first.metric.length) { @@ -102,6 +125,8 @@ void _trimPathSync( _appendPathSegmentSync(metric, result, trimStop, length); } if (trimStart > 0.0) { + // Make sure to connect the two paths (startWithMoveTo false) if we + // extracted the start. Force start with a move if the path is open. _appendPathSegmentSync(metric, result, 0.0, trimStart, startWithMoveTo: !extractStart || !metric.isClosed); } diff --git a/lib/src/rive_core/shapes/parametric_path.dart b/lib/src/rive_core/shapes/parametric_path.dart index 7f38e77..266e548 100644 --- a/lib/src/rive_core/shapes/parametric_path.dart +++ b/lib/src/rive_core/shapes/parametric_path.dart @@ -5,14 +5,19 @@ export 'package:rive/src/generated/shapes/parametric_path_base.dart'; abstract class ParametricPath extends ParametricPathBase { @override bool get isClosed => true; + @override Mat2D get pathTransform => worldTransform; + @override Mat2D get inversePathTransform => inverseWorldTransform; + @override void widthChanged(double from, double to) => markPathDirty(); + @override void heightChanged(double from, double to) => markPathDirty(); + @override void xChanged(double from, double to) { super.xChanged(from, to); @@ -45,6 +50,7 @@ abstract class ParametricPath extends ParametricPathBase { @override void originXChanged(double from, double to) => markPathDirty(); + @override void originYChanged(double from, double to) => markPathDirty(); } diff --git a/lib/src/rive_core/shapes/path.dart b/lib/src/rive_core/shapes/path.dart index 451993e..26bfe2f 100644 --- a/lib/src/rive_core/shapes/path.dart +++ b/lib/src/rive_core/shapes/path.dart @@ -1,5 +1,6 @@ import 'dart:math'; import 'dart:ui' as ui; + import 'package:rive/src/rive_core/component.dart'; import 'package:rive/src/rive_core/component_dirt.dart'; import 'package:rive/src/rive_core/component_flags.dart'; @@ -13,8 +14,11 @@ import 'package:rive/src/rive_core/shapes/straight_vertex.dart'; import 'package:rive/src/generated/shapes/path_base.dart'; export 'package:rive/src/generated/shapes/path_base.dart'; +/// An abstract low level path that gets implemented by parametric and point +/// based paths. abstract class Path extends PathBase { final Mat2D _inverseWorldTransform = Mat2D(); + final RenderPath _renderPath = RenderPath(); ui.Path get uiPath { if (!_isValid) { @@ -24,12 +28,17 @@ abstract class Path extends PathBase { } bool _isValid = false; + bool get isClosed; + Shape? _shape; + Shape? get shape => _shape; + Mat2D get pathTransform; Mat2D get inversePathTransform; Mat2D get inverseWorldTransform => _inverseWorldTransform; + @override bool resolveArtboard() { _changeShape(null); @@ -55,7 +64,10 @@ abstract class Path extends PathBase { @override void onRemoved() { + // We're no longer a child of the shape we may have been under, make sure to + // let it know we're gone. _changeShape(null); + super.onRemoved(); } @@ -63,7 +75,12 @@ abstract class Path extends PathBase { void updateWorldTransform() { super.updateWorldTransform(); _shape?.pathChanged(this); + + // Paths store their inverse world so that it's available for skinning and + // other operations that occur at runtime. if (!Mat2D.invert(_inverseWorldTransform, pathTransform)) { + // If for some reason the inversion fails (like we have a 0 scale) just + // store the identity. Mat2D.setIdentity(_inverseWorldTransform); } } @@ -71,11 +88,16 @@ abstract class Path extends PathBase { @override void update(int dirt) { super.update(dirt); + if (dirt & ComponentDirt.path != 0) { _buildPath(); } } + /// Subclasses should call this whenever a parameter that affects the topology + /// of the path changes in order to allow the system to rebuild the parametric + /// path. + /// should @internal when supported void markPathDirty() { addDirt(ComponentDirt.path); _isValid = false; @@ -83,6 +105,7 @@ abstract class Path extends PathBase { } List get vertices; + bool _buildPath() { _isValid = true; _renderPath.reset(); @@ -91,12 +114,15 @@ abstract class Path extends PathBase { if (length < 2) { return false; } + var firstPoint = vertices.first; double outX, outY; bool prevIsCubic; + double startX, startY; double startInX, startInY; bool startIsCubic; + if (firstPoint is CubicVertex) { startIsCubic = prevIsCubic = true; var inPoint = firstPoint.renderIn; @@ -112,29 +138,39 @@ abstract class Path extends PathBase { } else { startIsCubic = prevIsCubic = false; var point = firstPoint as StraightVertex; + var radius = point.radius; if (radius > 0) { var prev = vertices[length - 1]; + var pos = point.renderTranslation; + var toPrev = Vec2D.subtract(Vec2D(), prev is CubicVertex ? prev.renderOut : prev.renderTranslation, pos); var toPrevLength = Vec2D.length(toPrev); toPrev[0] /= toPrevLength; toPrev[1] /= toPrevLength; + var next = vertices[1]; + var toNext = Vec2D.subtract(Vec2D(), next is CubicVertex ? next.renderIn : next.renderTranslation, pos); var toNextLength = Vec2D.length(toNext); toNext[0] /= toNextLength; toNext[1] /= toNextLength; + var renderRadius = min(toPrevLength, min(toNextLength, radius)); + var translation = Vec2D.scaleAndAdd(Vec2D(), pos, toPrev, renderRadius); _renderPath.moveTo(startInX = startX = translation[0], startInY = startY = translation[1]); + var outPoint = Vec2D.scaleAndAdd( Vec2D(), pos, toPrev, icircleConstant * renderRadius); + var inPoint = Vec2D.scaleAndAdd( Vec2D(), pos, toNext, icircleConstant * renderRadius); + var posNext = Vec2D.scaleAndAdd(Vec2D(), pos, toNext, renderRadius); _renderPath.cubicTo(outPoint[0], outPoint[1], inPoint[0], inPoint[1], outX = posNext[0], outY = posNext[1]); @@ -146,28 +182,35 @@ abstract class Path extends PathBase { _renderPath.moveTo(startInX = startX = outX, startInY = startY = outY); } } + for (int i = 1; i < length; i++) { var vertex = vertices[i]; + if (vertex is CubicVertex) { var inPoint = vertex.renderIn; var translation = vertex.renderTranslation; _renderPath.cubicTo( outX, outY, inPoint[0], inPoint[1], translation[0], translation[1]); + prevIsCubic = true; var outPoint = vertex.renderOut; outX = outPoint[0]; outY = outPoint[1]; } else { var point = vertex as StraightVertex; + var radius = point.radius; if (radius > 0) { var pos = point.renderTranslation; + var toPrev = Vec2D.subtract(Vec2D(), Vec2D.fromValues(outX, outY), pos); var toPrevLength = Vec2D.length(toPrev); toPrev[0] /= toPrevLength; toPrev[1] /= toPrevLength; + var next = vertices[(i + 1) % length]; + var toNext = Vec2D.subtract( Vec2D(), next is CubicVertex ? next.renderIn : next.renderTranslation, @@ -175,7 +218,9 @@ abstract class Path extends PathBase { var toNextLength = Vec2D.length(toNext); toNext[0] /= toNextLength; toNext[1] /= toNextLength; + var renderRadius = min(toPrevLength, min(toNextLength, radius)); + var translation = Vec2D.scaleAndAdd(Vec2D(), pos, toPrev, renderRadius); if (prevIsCubic) { @@ -184,10 +229,13 @@ abstract class Path extends PathBase { } else { _renderPath.lineTo(translation[0], translation[1]); } + var outPoint = Vec2D.scaleAndAdd( Vec2D(), pos, toPrev, icircleConstant * renderRadius); + var inPoint = Vec2D.scaleAndAdd( Vec2D(), pos, toNext, icircleConstant * renderRadius); + var posNext = Vec2D.scaleAndAdd(Vec2D(), pos, toNext, renderRadius); _renderPath.cubicTo(outPoint[0], outPoint[1], inPoint[0], inPoint[1], outX = posNext[0], outY = posNext[1]); @@ -197,6 +245,7 @@ abstract class Path extends PathBase { var x = translation[0]; var y = translation[1]; _renderPath.cubicTo(outX, outY, x, y, x, y); + prevIsCubic = false; outX = x; outY = y; @@ -219,12 +268,14 @@ abstract class Path extends PathBase { @override void pathFlagsChanged(int from, int to) => markPathDirty(); + bool get isHidden => (pathFlags & ComponentFlags.hidden) != 0; } class RenderPath { final ui.Path _uiPath = ui.Path(); ui.Path get uiPath => _uiPath; + void reset() { _uiPath.reset(); } diff --git a/lib/src/rive_core/shapes/path_composer.dart b/lib/src/rive_core/shapes/path_composer.dart index 59e31c5..c85e6b9 100644 --- a/lib/src/rive_core/shapes/path_composer.dart +++ b/lib/src/rive_core/shapes/path_composer.dart @@ -1,22 +1,36 @@ import 'dart:ui' as ui; + import 'package:rive/src/rive_core/artboard.dart'; import 'package:rive/src/rive_core/component.dart'; import 'package:rive/src/rive_core/component_dirt.dart'; import 'package:rive/src/rive_core/math/mat2d.dart'; import 'package:rive/src/rive_core/shapes/shape.dart'; +/// The PathComposer builds the desired world and local paths for the shapes and +/// their fills/strokes. It guarantees that one of local or world path is always +/// available. If the Shape only wants a local path, we'll only build a local +/// one. If the Shape only wants a world path, we'll build only that world path. +/// If it wants both, we build both. If it wants none, we still build a world +/// path. class PathComposer extends Component { final Shape shape; PathComposer(this.shape); + @override Artboard? get artboard => shape.artboard; + final ui.Path worldPath = ui.Path(); final ui.Path localPath = ui.Path(); ui.Path _fillPath = ui.Path(); ui.Path get fillPath => _fillPath; + void _recomputePath() { + // No matter what we'll need some form of a world path to get our bounds. + // Let's optimize how we build it. var buildLocalPath = shape.wantLocalPath; var buildWorldPath = shape.wantWorldPath || !buildLocalPath; + + // The fill path will be whichever one of these two is available. if (buildLocalPath) { localPath.reset(); var world = shape.worldTransform; @@ -49,6 +63,9 @@ class PathComposer extends Component { @override void buildDependencies() { super.buildDependencies(); + + // We depend on the shape and all of its paths so that we can update after + // all of them. shape.addDependent(this); for (final path in shape.paths) { path.addDependent(this); diff --git a/lib/src/rive_core/shapes/path_vertex.dart b/lib/src/rive_core/shapes/path_vertex.dart index 7f4848a..d2e9508 100644 --- a/lib/src/rive_core/shapes/path_vertex.dart +++ b/lib/src/rive_core/shapes/path_vertex.dart @@ -1,4 +1,5 @@ import 'dart:typed_data'; + import 'package:rive/src/rive_core/bones/weight.dart'; import 'package:rive/src/rive_core/component.dart'; import 'package:rive/src/rive_core/math/mat2d.dart'; @@ -10,12 +11,16 @@ export 'package:rive/src/generated/shapes/path_vertex_base.dart'; abstract class PathVertex extends PathVertexBase { T? _weight; T? get weight => _weight; + Path? get path => parent as Path?; + @override void update(int dirt) {} + final Vec2D _renderTranslation = Vec2D(); Vec2D get translation => Vec2D.fromValues(x, y); Vec2D get renderTranslation => _renderTranslation; + set translation(Vec2D value) { x = value[0]; y = value[1]; @@ -31,12 +36,14 @@ abstract class PathVertex extends PathVertexBase { @override void xChanged(double from, double to) { _renderTranslation[0] = to; + path?.markPathDirty(); } @override void yChanged(double from, double to) { _renderTranslation[1] = to; + path?.markPathDirty(); } @@ -61,6 +68,7 @@ abstract class PathVertex extends PathVertexBase { } } + /// Deform only gets called when we are weighted. void deform(Mat2D world, Float32List boneTransforms) { Weight.deform(x, y, weight!.indices, weight!.values, world, boneTransforms, _weight!.translation); diff --git a/lib/src/rive_core/shapes/points_path.dart b/lib/src/rive_core/shapes/points_path.dart index 81b4468..be58fde 100644 --- a/lib/src/rive_core/shapes/points_path.dart +++ b/lib/src/rive_core/shapes/points_path.dart @@ -4,20 +4,29 @@ import 'package:rive/src/rive_core/component_dirt.dart'; import 'package:rive/src/rive_core/math/mat2d.dart'; import 'package:rive/src/rive_core/shapes/path_vertex.dart'; import 'package:rive/src/generated/shapes/points_path_base.dart'; + export 'package:rive/src/generated/shapes/points_path_base.dart'; class PointsPath extends PointsPathBase with Skinnable { final List _vertices = []; + PointsPath() { isClosed = false; } + + // When bound to bones pathTransform should be the identity as it'll already + // be in world space. @override Mat2D get pathTransform => skin != null ? Mat2D.identity : worldTransform; + + // When bound to bones inversePathTransform should be the identity. @override Mat2D get inversePathTransform => skin != null ? Mat2D() : inverseWorldTransform; + @override List get vertices => _vertices; + @override void childAdded(Component child) { super.childAdded(child); @@ -44,22 +53,33 @@ class PointsPath extends PointsPathBase with Skinnable { @override void buildDependencies() { super.buildDependencies(); + + // Depend on the skin, if we have it. This works because the skin is not a + // node so we have no dependency on our parent yet (which would cause a + // dependency cycle). skin?.addDependent(this); } @override void markPathDirty() { + // Make sure the skin gets marked dirty too. skin?.addDirt(ComponentDirt.path); super.markPathDirty(); } @override void markSkinDirty() => super.markPathDirty(); + @override void update(int dirt) { if (dirt & ComponentDirt.path != 0) { + // Before calling super (which will build the path) make sure to deform + // things if necessary. We depend on the skin which assures us that the + // boneTransforms are up to date. skin?.deform(_vertices); } + // Finally call super.update so the path commands can actually be rebuilt + // (when ComponentDirt.path is set). super.update(dirt); } } diff --git a/lib/src/rive_core/shapes/polygon.dart b/lib/src/rive_core/shapes/polygon.dart index bdfcb3c..07d7904 100644 --- a/lib/src/rive_core/shapes/polygon.dart +++ b/lib/src/rive_core/shapes/polygon.dart @@ -1,4 +1,5 @@ import 'dart:math'; + import 'package:rive/src/rive_core/shapes/path_vertex.dart'; import 'package:rive/src/rive_core/bones/weight.dart'; import 'package:rive/src/rive_core/shapes/straight_vertex.dart'; @@ -8,8 +9,10 @@ export 'package:rive/src/generated/shapes/polygon_base.dart'; class Polygon extends PolygonBase { @override void cornerRadiusChanged(double from, double to) => markPathDirty(); + @override void pointsChanged(int from, int to) => markPathDirty(); + @override List> get vertices { var vertexList = >[]; diff --git a/lib/src/rive_core/shapes/rectangle.dart b/lib/src/rive_core/shapes/rectangle.dart index b73c118..b72149c 100644 --- a/lib/src/rive_core/shapes/rectangle.dart +++ b/lib/src/rive_core/shapes/rectangle.dart @@ -4,10 +4,12 @@ import 'package:rive/src/generated/shapes/rectangle_base.dart'; export 'package:rive/src/generated/shapes/rectangle_base.dart'; class Rectangle extends RectangleBase { + // @override List get vertices { double ox = -originX * width; double oy = -originY * height; + return [ StraightVertex.procedural() ..x = ox @@ -24,18 +26,22 @@ class Rectangle extends RectangleBase { StraightVertex.procedural() ..x = ox ..y = oy + height - ..radius = linkCornerRadius ? cornerRadiusTL : cornerRadiusBL + ..radius = linkCornerRadius ? cornerRadiusTL : cornerRadiusBL, ]; } @override void cornerRadiusTLChanged(double from, double to) => markPathDirty(); + @override void cornerRadiusTRChanged(double from, double to) => markPathDirty(); + @override void cornerRadiusBLChanged(double from, double to) => markPathDirty(); + @override void cornerRadiusBRChanged(double from, double to) => markPathDirty(); + @override void linkCornerRadiusChanged(bool from, bool to) { markPathDirty(); diff --git a/lib/src/rive_core/shapes/shape.dart b/lib/src/rive_core/shapes/shape.dart index f1c9231..59dfc45 100644 --- a/lib/src/rive_core/shapes/shape.dart +++ b/lib/src/rive_core/shapes/shape.dart @@ -1,4 +1,5 @@ import 'dart:ui' as ui; + import 'package:rive/src/rive_core/component_dirt.dart'; import 'package:rive/src/rive_core/shapes/paint/linear_gradient.dart' as core; import 'package:rive/src/rive_core/shapes/paint/shape_paint_mutator.dart'; @@ -12,17 +13,21 @@ export 'package:rive/src/generated/shapes/shape_base.dart'; class Shape extends ShapeBase with ShapePaintContainer { final Set paths = {}; + bool _wantWorldPath = false; bool _wantLocalPath = false; bool get wantWorldPath => _wantWorldPath; bool get wantLocalPath => _wantLocalPath; bool _fillInWorld = false; bool get fillInWorld => _fillInWorld; + late PathComposer pathComposer; Shape() { pathComposer = PathComposer(this); } + ui.Path get fillPath => pathComposer.fillPath; + bool addPath(Path path) { paintChanged(); return paths.add(path); @@ -30,17 +35,29 @@ class Shape extends ShapeBase with ShapePaintContainer { void _markComposerDirty() { pathComposer.addDirt(ComponentDirt.path, recurse: true); + // Stroke effects need to be rebuilt whenever the path composer rebuilds the + // compound path. invalidateStrokeEffects(); } void pathChanged(Path path) => _markComposerDirty(); + void paintChanged() { addDirt(ComponentDirt.path); _markBlendModeDirty(); _markRenderOpacityDirty(); + + // Add world transform dirt to the direct dependents (don't recurse) as + // things like ClippingShape directly depend on their referenced Shape. This + // allows them to recompute any stored values which can change when the + // transformAffectsStroke property changes (whether the path is in world + // space or not). Consider using a different dirt type if this pattern is + // repeated. for (final d in dependents) { d.addDirt(ComponentDirt.worldTransform); } + + // Path composer needs to update if we update the types of paths we want. _markComposerDirty(); } @@ -59,6 +76,9 @@ class Shape extends ShapeBase with ShapePaintContainer { @override void update(int dirt) { super.update(dirt); + + // When the paint gets marked dirty, we need to sync the blend mode with the + // paints. if (dirt & ComponentDirt.blendMode != 0) { for (final fill in fills) { fill.blendMode = blendMode; @@ -67,6 +87,10 @@ class Shape extends ShapeBase with ShapePaintContainer { stroke.blendMode = blendMode; } } + + // RenderOpacity gets updated with the worldTransform (accumulates through + // hierarchy), so if we see worldTransform is dirty, update our internal + // render opacities. if (dirt & ComponentDirt.worldTransform != 0) { for (final fill in fills) { fill.renderOpacity = renderOpacity; @@ -75,7 +99,11 @@ class Shape extends ShapeBase with ShapePaintContainer { stroke.renderOpacity = renderOpacity; } } + // We update before the path composer so let's get our ducks in a row, what + // do we want? PathComposer depends on us so we're safe to update our + // desires here. if (dirt & ComponentDirt.path != 0) { + // Recompute which paths we want. _wantWorldPath = false; _wantLocalPath = false; for (final stroke in strokes) { @@ -85,20 +113,33 @@ class Shape extends ShapeBase with ShapePaintContainer { _wantWorldPath = true; } } + + // Update the gradients' paintsInWorldSpace properties based on whether + // the path we'll be feeding that at draw time is in world or local space. + // This is a good opportunity to do it as gradients depend on us so + // they'll update after us. + + // We optmistically first fill in the space we know the stroke will be in. _fillInWorld = _wantWorldPath || !_wantLocalPath; + + // Gradients almost always fill in local space, unless they are bound to + // bones. var mustFillLocal = fills.firstWhereOrNull( - (fill) => fill.paintMutator is core.LinearGradient) != + (fill) => fill.paintMutator is core.LinearGradient, + ) != null; if (mustFillLocal) { _fillInWorld = false; _wantLocalPath = true; } + for (final fill in fills) { var mutator = fill.paintMutator; if (mutator is core.LinearGradient) { mutator.paintsInWorldSpace = _fillInWorld; } } + for (final stroke in strokes) { var mutator = stroke.paintMutator; if (mutator is core.LinearGradient) { @@ -115,6 +156,7 @@ class Shape extends ShapeBase with ShapePaintContainer { @override void blendModeValueChanged(int from, int to) => _markBlendModeDirty(); + @override void draw(ui.Canvas canvas) { bool clipped = clip(canvas); @@ -129,12 +171,20 @@ class Shape extends ShapeBase with ShapePaintContainer { if (!_fillInWorld) { canvas.restore(); } + + // Strokes are slightly more complicated, they may want a local path. Note + // that we've already built this up during our update and processed any + // gradients to have their offsets in the correct transform space (see our + // update method). for (final stroke in strokes) { + // stroke.draw(canvas, _pathComposer); var transformAffectsStroke = stroke.transformAffectsStroke; var path = transformAffectsStroke ? pathComposer.localPath : pathComposer.worldPath; + if (transformAffectsStroke) { + // Get into world space. canvas.save(); canvas.transform(worldTransform.mat4); stroke.draw(canvas, path); @@ -143,6 +193,7 @@ class Shape extends ShapeBase with ShapePaintContainer { stroke.draw(canvas, path); } } + if (clipped) { canvas.restore(); } @@ -150,15 +201,22 @@ class Shape extends ShapeBase with ShapePaintContainer { void _markBlendModeDirty() => addDirt(ComponentDirt.blendMode); void _markRenderOpacityDirty() => addDirt(ComponentDirt.worldTransform); + @override void onPaintMutatorChanged(ShapePaintMutator mutator) { + // The transform affects stroke property may have changed as we have a new + // mutator. paintChanged(); } @override void onStrokesChanged() => paintChanged(); + @override void onFillsChanged() => paintChanged(); + + /// Since the PathComposer isn't in core, we need to let it know when to proxy + /// build dependencies. @override void buildDependencies() { super.buildDependencies(); diff --git a/lib/src/rive_core/shapes/shape_paint_container.dart b/lib/src/rive_core/shapes/shape_paint_container.dart index d24944e..a570b43 100644 --- a/lib/src/rive_core/shapes/shape_paint_container.dart +++ b/lib/src/rive_core/shapes/shape_paint_container.dart @@ -1,19 +1,33 @@ import 'package:rive/src/rive_core/component.dart'; import 'package:rive/src/rive_core/math/mat2d.dart'; import 'package:rive/src/rive_core/math/vec2d.dart'; + import 'package:rive/src/rive_core/shapes/paint/fill.dart'; import 'package:meta/meta.dart'; import 'package:rive/src/rive_core/shapes/paint/shape_paint_mutator.dart'; import 'package:rive/src/rive_core/shapes/paint/stroke.dart'; +/// An abstraction to give a common interface to any component that can contain +/// fills and strokes. abstract class ShapePaintContainer { final Set fills = {}; + final Set strokes = {}; + + /// Called whenever a new paint mutator is added/removed from the shape paints + /// (for example a linear gradient is added to a stroke). void onPaintMutatorChanged(ShapePaintMutator mutator); + + /// Called when a fill is added or removed. @protected void onFillsChanged(); + + /// Called when a stroke is added or remoevd. @protected void onStrokesChanged(); + + /// Called whenever the compound path for this shape is changed so that the + /// effects can be invalidated on all the strokes. void invalidateStrokeEffects() { for (final stroke in strokes) { stroke.invalidateEffects(); @@ -52,7 +66,11 @@ abstract class ShapePaintContainer { return false; } + /// These usually gets auto implemented as this mixin is meant to be added to + /// a ComponentBase. This way the implementor doesn't need to cast + /// ShapePaintContainer to ContainerComponent/Shape/Artboard/etc. bool addDirt(int value, {bool recurse = false}); + bool addDependent(Component dependent); void appendChild(Component child); Mat2D get worldTransform; diff --git a/lib/src/rive_core/shapes/star.dart b/lib/src/rive_core/shapes/star.dart index bd5c784..23a977c 100644 --- a/lib/src/rive_core/shapes/star.dart +++ b/lib/src/rive_core/shapes/star.dart @@ -1,4 +1,5 @@ import 'dart:math'; + import 'package:rive/src/rive_core/bones/weight.dart'; import 'package:rive/src/rive_core/shapes/path_vertex.dart'; import 'package:rive/src/rive_core/shapes/straight_vertex.dart'; @@ -8,6 +9,7 @@ export 'package:rive/src/generated/shapes/star_base.dart'; class Star extends StarBase { @override void innerRadiusChanged(double from, double to) => markPathDirty(); + @override List> get vertices { var actualPoints = points * 2; diff --git a/lib/src/rive_core/shapes/straight_vertex.dart b/lib/src/rive_core/shapes/straight_vertex.dart index ad3a715..55a130f 100644 --- a/lib/src/rive_core/shapes/straight_vertex.dart +++ b/lib/src/rive_core/shapes/straight_vertex.dart @@ -6,13 +6,20 @@ import 'package:rive/src/generated/shapes/straight_vertex_base.dart'; export 'package:rive/src/generated/shapes/straight_vertex_base.dart'; class StraightVertex extends StraightVertexBase { + /// Nullable because not all vertices have weight, they only have it when the + /// shape they are in is bound to bones. Weight? _weight; + StraightVertex(); + + /// Makes a vertex that is disconnected from core. StraightVertex.procedural() { InternalCoreHelper.markValid(this); } + @override String toString() => 'x[$x], y[$y], r[$radius]'; + @override void radiusChanged(double from, double to) { path?.markPathDirty(); diff --git a/lib/src/rive_core/shapes/triangle.dart b/lib/src/rive_core/shapes/triangle.dart index 1ce3a5e..b218f63 100644 --- a/lib/src/rive_core/shapes/triangle.dart +++ b/lib/src/rive_core/shapes/triangle.dart @@ -1,6 +1,8 @@ import 'package:rive/src/rive_core/shapes/path_vertex.dart'; import 'package:rive/src/rive_core/shapes/straight_vertex.dart'; import 'package:rive/src/generated/shapes/triangle_base.dart'; + +/// Export the Base class for external use (e.g. rive.dart) export 'package:rive/src/generated/shapes/triangle_base.dart'; class Triangle extends TriangleBase { @@ -8,6 +10,7 @@ class Triangle extends TriangleBase { List get vertices { double ox = -originX * width; double oy = -originY * height; + return [ StraightVertex.procedural() ..x = ox + width / 2 diff --git a/lib/src/rive_core/state_machine_controller.dart b/lib/src/rive_core/state_machine_controller.dart index b1e395c..8533202 100644 --- a/lib/src/rive_core/state_machine_controller.dart +++ b/lib/src/rive_core/state_machine_controller.dart @@ -1,9 +1,12 @@ import 'dart:collection'; + import 'package:rive/src/core/core.dart'; import 'package:flutter/foundation.dart'; -import 'package:rive/src/rive_core/animation/animation_state.dart'; +import 'package:rive/src/rive_core/animation/animation_state_instance.dart'; +import 'package:rive/src/rive_core/animation/any_state.dart'; import 'package:rive/src/rive_core/animation/layer_state.dart'; -import 'package:rive/src/rive_core/animation/linear_animation_instance.dart'; +import 'package:rive/src/rive_core/animation/linear_animation.dart'; +import 'package:rive/src/rive_core/animation/state_instance.dart'; import 'package:rive/src/rive_core/animation/state_machine.dart'; import 'package:rive/src/rive_core/animation/state_machine_layer.dart'; import 'package:rive/src/rive_core/animation/state_transition.dart'; @@ -11,21 +14,28 @@ import 'package:rive/src/rive_core/rive_animation_controller.dart'; class LayerController { final StateMachineLayer layer; - LayerState? _currentState; - LayerState? _stateFrom; + final StateInstance anyStateInstance; + + StateInstance? _currentState; + StateInstance? _stateFrom; bool _holdAnimationFrom = false; - LinearAnimationInstance? _animationInstanceFrom; StateTransition? _transition; double _mix = 1.0; - LinearAnimationInstance? _animationInstance; - LayerController(this.layer) { + + LayerController(this.layer) + : assert(layer.anyState != null), + anyStateInstance = layer.anyState!.makeInstance() { _changeState(layer.entryState); } + bool _changeState(LayerState? state, {StateTransition? transition}) { - if (state == _currentState) { + assert(state is! AnyState, + 'We don\'t allow making the AnyState an active state.'); + if (state == _currentState?.state) { return false; } - _currentState = state; + + _currentState = state?.makeInstance(); return true; } @@ -33,11 +43,17 @@ class LayerController { _changeState(null); } + bool get isTransitioning => + _transition != null && + _stateFrom != null && + _transition!.duration != 0 && + _mix != 1; + void _updateMix(double elapsedSeconds) { if (_transition != null && _stateFrom != null && _transition!.duration != 0) { - _mix = (_mix + elapsedSeconds / _transition!.mixTime(_stateFrom!)) + _mix = (_mix + elapsedSeconds / _transition!.mixTime(_stateFrom!.state)) .clamp(0, 1) .toDouble(); } else { @@ -45,103 +61,112 @@ class LayerController { } } + void _apply(CoreContext core) { + if (_holdAnimation != null) { + _holdAnimation!.apply(_holdTime, coreContext: core, mix: _holdMix); + _holdAnimation = null; + } + + if (_stateFrom != null && _mix < 1) { + _stateFrom!.apply(core, 1 - _mix); + } + if (_currentState != null) { + _currentState!.apply(core, _mix); + } + } + bool apply(StateMachineController machineController, CoreContext core, double elapsedSeconds, HashMap inputValues) { - if (_animationInstance != null) { - _animationInstance!.advance(elapsedSeconds); + if (_currentState != null) { + _currentState!.advance(elapsedSeconds, inputValues); } + _updateMix(elapsedSeconds); - if (_animationInstanceFrom != null && _mix < 1) { + + if (_stateFrom != null && _mix < 1) { + // This didn't advance during our updateState, but it should now that we + // realize we need to mix it in. if (!_holdAnimationFrom) { - _animationInstanceFrom!.advance(elapsedSeconds); + _stateFrom!.advance(elapsedSeconds, inputValues); } } - for (int i = 0; updateState(inputValues); i++) { - machineController.advanceInputs(); + + for (int i = 0; updateState(inputValues, i != 0); i++) { + _apply(core); + if (i == 100) { + // Escape hatch, let the user know their logic is causing some kind of + // recursive condition. print('StateMachineController.apply exceeded max iterations.'); + return false; } } - if (_animationInstanceFrom != null && _mix < 1) { - _animationInstanceFrom!.animation.apply(_animationInstanceFrom!.time, - mix: 1 - _mix, coreContext: core); - } - if (_animationInstance != null) { - _animationInstance!.animation - .apply(_animationInstance!.time, mix: _mix, coreContext: core); - } - return _mix != 1 || (_animationInstance?.keepGoing ?? false); + + _apply(core); + + return _mix != 1 || _waitingForExit || (_currentState?.keepGoing ?? false); } - bool updateState(HashMap inputValues) { - if (tryChangeState(layer.anyState, inputValues)) { + bool _waitingForExit = false; + LinearAnimation? _holdAnimation; + double _holdTime = 0; + double _holdMix = 0; + + bool updateState(HashMap inputValues, bool ignoreTriggers) { + if (isTransitioning) { + return false; + } + _waitingForExit = false; + if (tryChangeState(anyStateInstance, inputValues, ignoreTriggers)) { return true; } - return tryChangeState(_currentState, inputValues); + + return tryChangeState(_currentState, inputValues, ignoreTriggers); } - bool tryChangeState( - LayerState? stateFrom, HashMap inputValues) { + bool tryChangeState(StateInstance? stateFrom, + HashMap inputValues, bool ignoreTriggers) { if (stateFrom == null) { 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)) { - valid = false; - 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)) { + + for (final transition in stateFrom.state.transitions) { + var allowed = transition.allowed(stateFrom, inputValues, ignoreTriggers); + if (allowed == AllowTransition.yes && + _changeState(transition.stateTo, transition: transition)) { + // Take transition _transition = transition; _stateFrom = stateFrom; - if (transition.pauseOnExit && - transition.enableExitTime && - _animationInstance != null) { - _animationInstance!.time = - transition.exitTimeSeconds(stateFrom, absolute: true); + + // If we had an exit time and wanted to pause on exit, make sure to hold + // the exit time. Delegate this to the transition by telling it that it + // was completed. + if (transition.applyExitCondition(stateFrom)) { + // Make sure we apply this state. + var inst = (stateFrom as AnimationStateInstance).animationInstance; + _holdAnimation = inst.animation; + _holdTime = inst.time; + _holdMix = _mix; } + + // Keep mixing last animation that was mixed in. if (_mix != 0) { _holdAnimationFrom = transition.pauseOnExit; - _animationInstanceFrom = _animationInstance; } - if (_currentState is AnimationState) { - var animationState = _currentState as AnimationState; - var spilledTime = _animationInstanceFrom?.spilledTime ?? 0; - if (animationState.animation != null) { - _animationInstance = - LinearAnimationInstance(animationState.animation!); - _animationInstance!.advance(spilledTime); - } else { - _animationInstance = null; - } - _mix = 0; - _updateMix(0.0); - } else { - _animationInstance = null; + if (stateFrom is AnimationStateInstance) { + var spilledTime = stateFrom.animationInstance.spilledTime; + _currentState?.advance(spilledTime, inputValues); } + + _mix = 0; + _updateMix(0); + // Make sure to reset _waitingForExit to false if we succeed at taking a + // transition. + _waitingForExit = false; return true; + } else if (allowed == AllowTransition.waitingForExit) { + _waitingForExit = true; } } return false; @@ -153,6 +178,7 @@ class StateMachineController extends RiveAnimationController { final inputValues = HashMap(); StateMachineController(this.stateMachine); final layerControllers = []; + void _clearLayerControllers() { for (final layer in layerControllers) { layer.dispose(); @@ -163,10 +189,14 @@ class StateMachineController extends RiveAnimationController { @override bool init(CoreContext core) { _clearLayerControllers(); + for (final layer in stateMachine.layers) { layerControllers.add(LayerController(layer)); } + + // Make sure triggers are all reset. advanceInputs(); + return super.init(core); } @@ -178,6 +208,7 @@ class StateMachineController extends RiveAnimationController { @protected void advanceInputs() {} + @override void apply(CoreContext core, double elapsedSeconds) { bool keepGoing = false; @@ -186,6 +217,7 @@ class StateMachineController extends RiveAnimationController { keepGoing = true; } } + advanceInputs(); isActive = keepGoing; } } diff --git a/lib/src/rive_core/state_transition_flags.dart b/lib/src/rive_core/state_transition_flags.dart index b6f0afd..d30f426 100644 --- a/lib/src/rive_core/state_transition_flags.dart +++ b/lib/src/rive_core/state_transition_flags.dart @@ -1,7 +1,17 @@ class StateTransitionFlags { + /// Whether the transition is disabled. static const int disabled = 1 << 0; + + /// Whether the transition duration is a percentage or time in ms. static const int durationIsPercentage = 1 << 1; + + /// Whether exit time is enabled. static const int enableExitTime = 1 << 2; + + /// Whether the exit time is a percentage or time in ms. static const int exitTimeIsPercentage = 1 << 3; + + /// Whether the animation is held at exit or if it keeps advancing during + /// mixing. static const int pauseOnExit = 1 << 4; } diff --git a/lib/src/rive_core/transform_component.dart b/lib/src/rive_core/transform_component.dart index f4a4d03..b3bc8a2 100644 --- a/lib/src/rive_core/transform_component.dart +++ b/lib/src/rive_core/transform_component.dart @@ -12,21 +12,29 @@ import 'package:rive/src/generated/transform_component_base.dart'; export 'package:rive/src/generated/transform_component_base.dart'; abstract class TransformComponent extends TransformComponentBase { + /// Draw rules saved against this transform component, inherited by children. DrawRules? _drawRules; + DrawRules? get drawRules => _drawRules; + final List _clippingShapes = []; Iterable get clippingShapes => _clippingShapes; + double _renderOpacity = 0; double get renderOpacity => _renderOpacity; + final Mat2D worldTransform = Mat2D(); final Mat2D transform = Mat2D(); + Vec2D get translation => Vec2D.fromValues(x, y); Vec2D get worldTranslation => Vec2D.fromValues(worldTransform[4], worldTransform[5]); + double get x; double get y; set x(double value); set y(double value); + @override void update(int dirt) { if (dirt & ComponentDirt.transform != 0) { @@ -45,10 +53,14 @@ abstract class TransformComponent extends TransformComponentBase { } transform[4] = x; transform[5] = y; + Mat2D.scaleByValues(transform, scaleX, scaleY); } + // TODO: when we have layer effect renderers, this will need to render 1 for + // layer effects. double get childOpacity => _renderOpacity; + Vec2D get scale => Vec2D.fromValues(scaleX, scaleY); set scale(Vec2D value) { scaleX = value[0]; @@ -70,6 +82,7 @@ abstract class TransformComponent extends TransformComponentBase { void calculateWorldTransform() { var parent = this.parent; final chain = [this]; + while (parent != null) { if (parent is TransformComponent) { chain.insert(0, parent); @@ -131,10 +144,12 @@ abstract class TransformComponent extends TransformComponentBase { switch (child.coreType) { case DrawRulesBase.typeKey: _drawRules = child as DrawRules; + break; case ClippingShapeBase.typeKey: _clippingShapes.add(child as ClippingShape); addDirt(ComponentDirt.clip, recurse: true); + break; } } diff --git a/lib/src/rive_file.dart b/lib/src/rive_file.dart index 3018e32..3dde563 100644 --- a/lib/src/rive_file.dart +++ b/lib/src/rive_file.dart @@ -6,6 +6,7 @@ import 'package:rive/src/core/core.dart'; import 'package:rive/src/core/field_types/core_field_type.dart'; import 'package:rive/src/generated/animation/animation_state_base.dart'; import 'package:rive/src/generated/animation/any_state_base.dart'; +import 'package:rive/src/generated/animation/blend_state_transition_base.dart'; import 'package:rive/src/generated/animation/entry_state_base.dart'; import 'package:rive/src/generated/animation/exit_state_base.dart'; import 'package:rive/src/generated/animation/keyed_property_base.dart'; @@ -23,6 +24,8 @@ import 'package:rive/src/rive_core/runtime/exceptions/rive_format_error_exceptio import 'package:rive/src/rive_core/runtime/runtime_header.dart'; import 'package:rive/src/utilities/binary_buffer/binary_reader.dart'; +import 'generated/animation/blend_state_1d_base.dart'; +import 'generated/animation/blend_state_direct_base.dart'; import 'rive_core/animation/state_transition.dart'; Core? _readRuntimeObject( @@ -140,27 +143,26 @@ class RiveFile { stackObject = StateMachineImporter(object as StateMachine); break; case StateMachineLayerBase.typeKey: - { - // Needs artboard importer to resolve linear animations. - var artboardImporter = importStack - .requireLatest(ArtboardBase.typeKey); - stackObject = StateMachineLayerImporter( - object as StateMachineLayer, artboardImporter); - break; - } + stackObject = StateMachineLayerImporter(object as StateMachineLayer); + break; + case EntryStateBase.typeKey: case AnyStateBase.typeKey: case ExitStateBase.typeKey: case AnimationStateBase.typeKey: + case BlendStateDirectBase.typeKey: + case BlendState1DBase.typeKey: stackObject = LayerStateImporter(object as LayerState); stackType = LayerStateBase.typeKey; break; case StateTransitionBase.typeKey: + case BlendStateTransitionBase.typeKey: { var stateMachineImporter = importStack .requireLatest(StateMachineBase.typeKey); stackObject = StateTransitionImporter( object as StateTransition, stateMachineImporter); + stackType = StateTransitionBase.typeKey; break; } default: diff --git a/lib/src/state_machine_controller.dart b/lib/src/state_machine_controller.dart index 65ce821..cec66d8 100644 --- a/lib/src/state_machine_controller.dart +++ b/lib/src/state_machine_controller.dart @@ -1,5 +1,4 @@ import 'package:flutter/foundation.dart'; -import 'package:rive/src/core/core.dart'; import 'package:rive/src/generated/animation/state_machine_bool_base.dart'; import 'package:rive/src/generated/animation/state_machine_number_base.dart'; import 'package:rive/src/generated/animation/state_machine_trigger_base.dart'; @@ -160,14 +159,6 @@ class StateMachineController extends core.StateMachineController { return null; } - @override - void apply(CoreContext core, double elapsedSeconds) { - super.apply(core, elapsedSeconds); - for (final input in _inputs) { - input.advance(); - } - } - @override void advanceInputs() { for (final input in _inputs) {