import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../components/base_component.dart'; import '../components/position_component.dart'; import '../extensions/vector2.dart'; export './move_effect.dart'; export './rotate_effect.dart'; export './scale_effect.dart'; export './sequence_effect.dart'; abstract class ComponentEffect { T component; Function() onComplete; bool _isDisposed = false; bool get isDisposed => _isDisposed; bool _isPaused = false; bool get isPaused => _isPaused; void resume() => _isPaused = false; void pause() => _isPaused = true; /// If the animation should first follow the initial curve and then follow the /// curve backwards bool isInfinite; bool isAlternating; final bool isRelative; final bool _initialIsInfinite; final bool _initialIsAlternating; double percentage; double curveProgress = 0.0; double peakTime = 0.0; double currentTime = 0.0; double driftTime = 0.0; int curveDirection = 1; Curve curve; double get iterationTime => peakTime * (isAlternating ? 2 : 1); ComponentEffect( this._initialIsInfinite, this._initialIsAlternating, { this.isRelative = false, Curve curve, this.onComplete, }) : assert(isRelative != null), isInfinite = _initialIsInfinite, isAlternating = _initialIsAlternating, curve = curve ?? Curves.linear; @mustCallSuper void update(double dt) { if (isAlternating) { curveDirection = isMax() ? -1 : (isMin() ? 1 : curveDirection); } if (isInfinite) { if ((!isAlternating && isMax()) || (isAlternating && isMin())) { reset(); } } if (!hasCompleted()) { currentTime += (dt + driftTime) * curveDirection; percentage = (currentTime / peakTime).clamp(0.0, 1.0).toDouble(); curveProgress = curve.transform(percentage); _updateDriftTime(); currentTime = currentTime.clamp(0.0, peakTime).toDouble(); } } @mustCallSuper void initialize(T component) { this.component = component; } void dispose() => _isDisposed = true; bool hasCompleted() { return (!isInfinite && !isAlternating && isMax()) || (!isInfinite && isAlternating && isMin()) || isDisposed; } bool isMax() => percentage == null ? false : percentage == 1.0; bool isMin() => percentage == null ? false : percentage == 0.0; bool isRootEffect() => component?.effects?.contains(this) ?? false; void reset() { _isDisposed = false; percentage = null; currentTime = 0.0; curveDirection = 1; isInfinite = _initialIsInfinite; isAlternating = _initialIsAlternating; setComponentToOriginalState(); } // When the time overshoots the max and min it needs to add that time to // whatever is going to happen next, for example an alternation or // following effect in a SequenceEffect. void _updateDriftTime() { if (isMax()) { driftTime = currentTime - peakTime; } else if (isMin()) { driftTime = currentTime.abs(); } else { driftTime = 0; } } void setComponentToOriginalState(); void setComponentToEndState(); } abstract class PositionComponentEffect extends ComponentEffect { /// Used to be able to determine the start state of the component Vector2 originalPosition; double originalAngle; Vector2 originalSize; /// Used to be able to determine the end state of a sequence of effects Vector2 endPosition; double endAngle; Vector2 endSize; /// Whether the state of a certain field was modified by the effect final bool modifiesPosition; final bool modifiesAngle; final bool modifiesSize; PositionComponentEffect( bool initialIsInfinite, bool initialIsAlternating, { bool isRelative = false, Curve curve, this.modifiesPosition = false, this.modifiesAngle = false, this.modifiesSize = false, void Function() onComplete, }) : super( initialIsInfinite, initialIsAlternating, isRelative: isRelative, curve: curve, onComplete: onComplete, ); @mustCallSuper @override void initialize(PositionComponent component) { super.initialize(component); this.component = component; originalPosition = component.position.clone(); originalAngle = component.angle; originalSize = component.size.clone(); /// If these aren't modified by the extending effect it is assumed that the /// effect didn't bring the component to another state than the one it /// started in endPosition = component.position.clone(); endAngle = component.angle; endSize = component.size.clone(); } /// Only change the parts of the component that is affected by the /// effect, and only set the state if it is the root effect (not part of /// another effect, like children of a CombinedEffect or SequenceEffect). void _setComponentState(Vector2 position, double angle, Vector2 size) { if (isRootEffect()) { if (modifiesPosition) { assert( position != null, '`position` must not be `null` for an effect which modifies `position`', ); component?.position?.setFrom(position); } if (modifiesAngle) { assert( angle != null, '`angle` must not be `null` for an effect which modifies `angle`', ); component?.angle = angle; } if (modifiesSize) { assert( size != null, '`size` must not be `null` for an effect which modifies `size`', ); component?.size?.setFrom(size); } } } @override void setComponentToOriginalState() { _setComponentState(originalPosition, originalAngle, originalSize); } @override void setComponentToEndState() { _setComponentState(endPosition, endAngle, endSize); } } abstract class SimplePositionComponentEffect extends PositionComponentEffect { double duration; double speed; SimplePositionComponentEffect( bool initialIsInfinite, bool initialIsAlternating, { this.duration, this.speed, Curve curve, bool isRelative = false, bool modifiesPosition = false, bool modifiesAngle = false, bool modifiesSize = false, void Function() onComplete, }) : assert( (duration != null) ^ (speed != null), "Either speed or duration necessary", ), super( initialIsInfinite, initialIsAlternating, isRelative: isRelative, curve: curve, modifiesPosition: modifiesPosition, modifiesAngle: modifiesAngle, modifiesSize: modifiesSize, onComplete: onComplete, ); }