From 1962ae76d4e6ae8d95a0db9ecd762546c2040e1f Mon Sep 17 00:00:00 2001 From: Daniil Vinogradov Date: Sun, 15 Dec 2024 00:41:14 +0100 Subject: [PATCH] Animations implemented --- .vscode/settings.json | 63 ++++- Submodules/UIKit/CMakeLists.txt | 8 + Submodules/UIKit/include/CABasicAnimation.h | 44 ++++ .../UIKit/include/CABasicAnimationPrototype.h | 24 ++ Submodules/UIKit/include/CALayer.h | 35 +++ .../UIKit/include/CAMediaTimingFunction.h | 45 ++++ Submodules/UIKit/include/CASpringAnimation.h | 19 ++ .../include/CASpringAnimationPrototype.h | 23 ++ Submodules/UIKit/include/CATransaction.h | 28 +++ Submodules/UIKit/include/UIColor.h | 4 + Submodules/UIKit/include/UIView.h | 33 ++- .../UIKit/include/UIViewAnimationGroup.h | 20 ++ .../UIKit/include/UIViewAnimationOptions.h | 17 ++ Submodules/UIKit/include/tools/Tools.hpp | 27 +++ Submodules/UIKit/lib/CABasicAnimation.cpp | 55 +++++ .../UIKit/lib/CABasicAnimationPrototype.cpp | 20 ++ Submodules/UIKit/lib/CALayer.cpp | 226 +++++++++++++++++- .../UIKit/lib/CAMediaTimingFunction.cpp | 81 +++++++ Submodules/UIKit/lib/CASpringAnimation.cpp | 19 ++ .../UIKit/lib/CASpringAnimationPrototype.cpp | 21 ++ Submodules/UIKit/lib/CATransaction.cpp | 35 +++ Submodules/UIKit/lib/UIApplicationMain.cpp | 4 +- Submodules/UIKit/lib/UIColor.cpp | 31 +++ Submodules/UIKit/lib/UIView.cpp | 87 +++++++ Submodules/UIKit/lib/UIViewAnimationGroup.cpp | 17 ++ .../UIKit/lib/UIViewAnimationOptions.cpp | 0 app/AppDelegate.cpp | 8 +- 27 files changed, 979 insertions(+), 15 deletions(-) create mode 100644 Submodules/UIKit/include/CABasicAnimationPrototype.h create mode 100644 Submodules/UIKit/include/CAMediaTimingFunction.h create mode 100644 Submodules/UIKit/include/CASpringAnimation.h create mode 100644 Submodules/UIKit/include/CASpringAnimationPrototype.h create mode 100644 Submodules/UIKit/include/CATransaction.h create mode 100644 Submodules/UIKit/include/UIViewAnimationGroup.h create mode 100644 Submodules/UIKit/include/UIViewAnimationOptions.h create mode 100644 Submodules/UIKit/include/tools/Tools.hpp create mode 100644 Submodules/UIKit/lib/CABasicAnimation.cpp create mode 100644 Submodules/UIKit/lib/CABasicAnimationPrototype.cpp create mode 100644 Submodules/UIKit/lib/CAMediaTimingFunction.cpp create mode 100644 Submodules/UIKit/lib/CASpringAnimation.cpp create mode 100644 Submodules/UIKit/lib/CASpringAnimationPrototype.cpp create mode 100644 Submodules/UIKit/lib/CATransaction.cpp create mode 100644 Submodules/UIKit/lib/UIViewAnimationGroup.cpp create mode 100644 Submodules/UIKit/lib/UIViewAnimationOptions.cpp diff --git a/.vscode/settings.json b/.vscode/settings.json index 787a758..3eb1fff 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,67 @@ "files.associations": { "utility": "cpp", "__locale": "cpp", - "*.inc": "cpp" + "*.inc": "cpp", + "functional": "cpp", + "__bit_reference": "cpp", + "__hash_table": "cpp", + "__node_handle": "cpp", + "__split_buffer": "cpp", + "__threading_support": "cpp", + "__verbose_abort": "cpp", + "array": "cpp", + "bitset": "cpp", + "cctype": "cpp", + "charconv": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "complex": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "execution": "cpp", + "memory": "cpp", + "forward_list": "cpp", + "initializer_list": "cpp", + "ios": "cpp", + "iosfwd": "cpp", + "istream": "cpp", + "limits": "cpp", + "locale": "cpp", + "mutex": "cpp", + "new": "cpp", + "optional": "cpp", + "ostream": "cpp", + "print": "cpp", + "queue": "cpp", + "ratio": "cpp", + "sstream": "cpp", + "stack": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "string": "cpp", + "string_view": "cpp", + "tuple": "cpp", + "typeinfo": "cpp", + "unordered_map": "cpp", + "variant": "cpp", + "vector": "cpp", + "algorithm": "cpp", + "__assertion_handler": "cpp", + "__config": "cpp", + "__debug": "cpp", + "__errc": "cpp", + "__mutex_base": "cpp", + "atomic": "cpp", + "exception": "cpp", + "system_error": "cpp", + "any": "cpp" } } \ No newline at end of file diff --git a/Submodules/UIKit/CMakeLists.txt b/Submodules/UIKit/CMakeLists.txt index 8e962ec..fc04ff2 100644 --- a/Submodules/UIKit/CMakeLists.txt +++ b/Submodules/UIKit/CMakeLists.txt @@ -5,6 +5,12 @@ add_definitions( ) add_library(UIKit + lib/CABasicAnimation.cpp + lib/CABasicAnimationPrototype.cpp + lib/CASpringAnimation.cpp + lib/CASpringAnimationPrototype.cpp + lib/CAMediaTimingFunction.cpp + lib/CATransaction.cpp lib/ContentsGravityTransformation.cpp lib/platforms/SkiaCtx.cpp lib/Application.cpp @@ -22,6 +28,8 @@ add_library(UIKit lib/UIImageView.cpp lib/UILabel.cpp lib/UIView.cpp + lib/UIViewAnimationGroup.cpp + lib/UIViewAnimationOptions.cpp lib/UIWindow.cpp lib/NXAffineTransform.cpp lib/NXTransform3D.cpp diff --git a/Submodules/UIKit/include/CABasicAnimation.h b/Submodules/UIKit/include/CABasicAnimation.h index e69de29..a5249e1 100644 --- a/Submodules/UIKit/include/CABasicAnimation.h +++ b/Submodules/UIKit/include/CABasicAnimation.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace NXKit { + +class CAAction {}; + +class CABasicAnimation: public CAAction { +public: + /// animation duration in seconds + double duration = 0; + + /// animation delay in seconds + double delay = 0; + + std::optional keyPath; + std::optional fillMode; + bool isRemovedOnCompletion = true; + std::shared_ptr timingFunction; + + std::optional fromValue; + std::optional toValue; + + std::shared_ptr animationGroup; + Timer creationTime; + + CABasicAnimation(std::string keyPath); + CABasicAnimation(std::shared_ptr prototype, std::string keyPath, AnimatableProperty fromValue, std::shared_ptr timingFunction); + CABasicAnimation(CABasicAnimation* animation); + + bool wasCreatedInUIAnimateBlock(); + float progressFor(Timer currentTime); + +private: + float ease(float x); +}; + +} + diff --git a/Submodules/UIKit/include/CABasicAnimationPrototype.h b/Submodules/UIKit/include/CABasicAnimationPrototype.h new file mode 100644 index 0000000..0ffe588 --- /dev/null +++ b/Submodules/UIKit/include/CABasicAnimationPrototype.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include +#include + +namespace NXKit { + +using AnimatableProperty = std::any; + +class CABasicAnimation; +class CABasicAnimationPrototype: public enable_shared_from_this { +public: + const double duration; + const double delay; + const std::shared_ptr animationGroup; + + CABasicAnimationPrototype(double duration, double delay, std::shared_ptr animationGroup); + + virtual std::shared_ptr createAnimation(std::string keyPath, AnimatableProperty fromValue); +}; + +} diff --git a/Submodules/UIKit/include/CALayer.h b/Submodules/UIKit/include/CALayer.h index 784ba48..2b3afd6 100644 --- a/Submodules/UIKit/include/CALayer.h +++ b/Submodules/UIKit/include/CALayer.h @@ -6,12 +6,14 @@ #include #include #include +#include #include #include #include #include #include +#include namespace NXKit { @@ -24,8 +26,11 @@ class CALayerDelegate { class CALayer: public enable_shared_from_this { public: CALayer(); + CALayer(CALayer* layer); ~CALayer() {} + std::weak_ptr delegate; + // Getter Setters void setContentsGravity(CALayerContentsGravity contentsGravity) { _contentsGravity = contentsGravity; } [[nodiscard]] CALayerContentsGravity contentsGravity() const { return _contentsGravity; } @@ -85,10 +90,29 @@ class CALayer: public enable_shared_from_this { void removeFromSuperlayer(); + CALayer* copy(); + + std::shared_ptr actionForKey(std::string event); + static std::shared_ptr defaultActionForKey(std::string event); + static NXFloat defaultAnimationDuration; + + std::shared_ptr createPresentation(); + std::shared_ptr presentation() { return _presentation; } + std::shared_ptr presentationOrSelf(); + + // Animations + void add(std::shared_ptr animation, std::string keyPath); + void removeAnimation(std::string forKey); + void removeAllAnimations(); void onWillSet(std::string keyPath); + void onDidSetAnimations(bool wasEmpty); + std::optional value(std::string forKeyPath); + + void animateAt(Timer currentTime); void skiaRender(SkCanvas* canvas); private: + friend class UIView; /// Defaults to 1.0 but if the layer is associated with a view, /// the view sets this value to match the screen. @@ -123,6 +147,17 @@ class CALayer: public enable_shared_from_this { often enough for us to care about it. **/ static bool layerTreeIsDirty; + + std::shared_ptr _presentation; + std::map> animations; + + bool isPresentationForAnotherLayer = false; + + /// We disable animation on parameters of views / layers that haven't been rendered yet. + /// This is both a performance optimization (avoids lots of animations at the start) + /// as well as a correctness fix (matches iOS behaviour). Maybe there's a better way though? + bool hasBeenRenderedInThisPartOfOverallLayerHierarchy = false; + void update(std::shared_ptr presentation, std::shared_ptr animation, float progress); }; } diff --git a/Submodules/UIKit/include/CAMediaTimingFunction.h b/Submodules/UIKit/include/CAMediaTimingFunction.h new file mode 100644 index 0000000..09451d9 --- /dev/null +++ b/Submodules/UIKit/include/CAMediaTimingFunction.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include + +namespace NXKit { + +// Note these are actually acceleration rates (explains why Fast is a smaller value than Normal) +const double UIScrollViewDecelerationRateNormal = 0.998; +const double UIScrollViewDecelerationRateFast = 0.99; + +const std::string kCAMediaTimingFunctionLinear = "linear"; +const std::string kCAMediaTimingFunctionEaseIn = "easeIn"; +const std::string kCAMediaTimingFunctionEaseOut = "easeOut"; +const std::string kCAMediaTimingFunctionEaseInEaseOut = "easeInEaseOut"; +const std::string kCAMediaTimingFunctionDefault = "default"; +const std::string kCAMediaTimingFunctionCustomEaseOut = "customEaseOut"; +const std::string kCAMediaTimingFunctionEaseOutElastic = "easeOutElastic"; + +class CAMediaTimingFunction { +public: + CAMediaTimingFunction(std::string name); + CAMediaTimingFunction(std::function timing); + + static double linear(double x); + static double easeInCubic(double x); + static double easeOutCubic(double x); + static double easeInQuad(double x); + static double easeOutQuad(double x); + static double easeInOutCubic(double x); + static double easeOutElastic(double x); + + // from CubicBezier1D optimising away constant terms + static double customEaseOut(double x); + static std::shared_ptr timingFunctionFrom(UIViewAnimationOptions options); + + float at(float x); +private: + std::function timing; +}; + +} + diff --git a/Submodules/UIKit/include/CASpringAnimation.h b/Submodules/UIKit/include/CASpringAnimation.h new file mode 100644 index 0000000..0fad21a --- /dev/null +++ b/Submodules/UIKit/include/CASpringAnimation.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace NXKit { + +class CASpringAnimation: public CABasicAnimation { +public: + std::optional damping; + std::optional initialSpringVelocity; + + CASpringAnimation(CASpringAnimation* animation); + CASpringAnimation(std::shared_ptr prototype, + std::string keyPath, + AnimatableProperty fromValue); +}; + +} diff --git a/Submodules/UIKit/include/CASpringAnimationPrototype.h b/Submodules/UIKit/include/CASpringAnimationPrototype.h new file mode 100644 index 0000000..5ae756c --- /dev/null +++ b/Submodules/UIKit/include/CASpringAnimationPrototype.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace NXKit { + +class CASpringAnimation; +class CASpringAnimationPrototype: public CABasicAnimationPrototype { +public: + const double damping; + const double initialSpringVelocity; + + CASpringAnimationPrototype(double duration, + double delay, + double damping, + double initialSpringVelocity, + std::shared_ptr animationGroup); + + std::shared_ptr createAnimation(std::string keyPath, AnimatableProperty fromValue) override; + +}; + +} diff --git a/Submodules/UIKit/include/CATransaction.h b/Submodules/UIKit/include/CATransaction.h new file mode 100644 index 0000000..3795f83 --- /dev/null +++ b/Submodules/UIKit/include/CATransaction.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +namespace NXKit { + +struct CATransaction { +private: + bool disableActions_ = false; + float animationDuration_ = CALayer::defaultAnimationDuration; + + static std::vector transactionStack; + +public: + static void begin(); + + static void commit(); + + static bool disableActions(); + + static void setDisableActions(bool newValue); + + static float animationDuration(); + + static void setAnimationDuration(float newValue); +}; + +} diff --git a/Submodules/UIKit/include/UIColor.h b/Submodules/UIKit/include/UIColor.h index 4d1d961..5b9d665 100644 --- a/Submodules/UIKit/include/UIColor.h +++ b/Submodules/UIKit/include/UIColor.h @@ -1,5 +1,7 @@ #pragma once +#include + namespace NXKit { class UIColor { @@ -15,6 +17,8 @@ class UIColor { bool operator==(const UIColor& rhs) const; + UIColor interpolationTo(UIColor endResult, NXFloat progress); + static UIColor clear; static UIColor red; static UIColor green; diff --git a/Submodules/UIKit/include/UIView.h b/Submodules/UIKit/include/UIView.h index e5ca311..b1e2d60 100644 --- a/Submodules/UIKit/include/UIView.h +++ b/Submodules/UIKit/include/UIView.h @@ -2,10 +2,11 @@ #include "CALayer.h" #include +#include namespace NXKit { -class UIView: public enable_shared_from_this { +class UIView: public CALayerDelegate, public enable_shared_from_this { public: UIView(): UIView(NXRect()) {} UIView(NXRect frame); @@ -52,6 +53,35 @@ class UIView: public enable_shared_from_this { virtual std::shared_ptr initLayer(); std::shared_ptr layer() const { return _layer; }; + + + // Animations + static std::set> layersWithAnimations; + static std::shared_ptr currentAnimationPrototype; + + static void animate(double duration, + double delay = 0.0, + UIViewAnimationOptions options = UIViewAnimationOptions::none, + std::function animations = [](){}, + std::function completion = [](bool res){}); + + static void animate(double duration, + std::function animations = [](){}); + + static void animate(double duration, + double delay, + double usingSpringWithDamping, + double initialSpringVelocity, + UIViewAnimationOptions options = UIViewAnimationOptions::none, + std::function animations = [](){}, + std::function completion = [](bool res){}); + + static void animateIfNeeded(Timer currentTime); + static void completePendingAnimations(); + + std::shared_ptr actionForKey(std::string event) override; +// virtual void draw() {} + virtual void display(std::shared_ptr layer) override; private: std::vector> _subviews; std::weak_ptr _superview; @@ -61,6 +91,7 @@ class UIView: public enable_shared_from_this { bool _isUserInteractionEnabled = true; void setSuperview(std::shared_ptr superview); + bool anyCurrentlyRunningAnimationsAllowUserInteraction(); }; } diff --git a/Submodules/UIKit/include/UIViewAnimationGroup.h b/Submodules/UIKit/include/UIViewAnimationGroup.h new file mode 100644 index 0000000..8b5a14b --- /dev/null +++ b/Submodules/UIKit/include/UIViewAnimationGroup.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +namespace NXKit { + +class UIViewAnimationGroup { +public: + std::optional> completion; + int queuedAnimations = 0; + UIViewAnimationOptions options; + + UIViewAnimationGroup(UIViewAnimationOptions options, std::optional> completion); + + void animationDidStop(bool finished); +}; + +} diff --git a/Submodules/UIKit/include/UIViewAnimationOptions.h b/Submodules/UIKit/include/UIViewAnimationOptions.h new file mode 100644 index 0000000..5e23990 --- /dev/null +++ b/Submodules/UIKit/include/UIViewAnimationOptions.h @@ -0,0 +1,17 @@ +#pragma once + +namespace NXKit { + +enum UIViewAnimationOptions { + none = 0, + allowUserInteraction = 1 << 0, + beginFromCurrentState = 1 << 1, + curveEaseIn = 1 << 2, + curveEaseOut = 1 << 3, + curveEaseInOut = 1 << 4, + curveLinear = 1 << 5, + curveEaseOutElastic = 1 << 6, + customEaseOut = 1 << 9 +}; + +} diff --git a/Submodules/UIKit/include/tools/Tools.hpp b/Submodules/UIKit/include/tools/Tools.hpp new file mode 100644 index 0000000..0f074f3 --- /dev/null +++ b/Submodules/UIKit/include/tools/Tools.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NXKit { + +template< typename T > +std::optional any_optional_cast(std::optional obj) { + if (!obj.has_value()) { return std::nullopt; } + + try { + return std::any_cast(obj.value()); // throws + } + catch(const std::bad_any_cast& e) { + return std::nullopt; + } +} + +} \ No newline at end of file diff --git a/Submodules/UIKit/lib/CABasicAnimation.cpp b/Submodules/UIKit/lib/CABasicAnimation.cpp new file mode 100644 index 0000000..7c381f1 --- /dev/null +++ b/Submodules/UIKit/lib/CABasicAnimation.cpp @@ -0,0 +1,55 @@ +#include + +using namespace NXKit; + +CABasicAnimation::CABasicAnimation(std::string keyPath) { + timingFunction = new_shared(kCAMediaTimingFunctionDefault); + this->keyPath = keyPath; +} + +CABasicAnimation::CABasicAnimation(std::shared_ptr prototype, std::string keyPath, AnimatableProperty fromValue, std::shared_ptr timingFunction): + CABasicAnimation(keyPath) +{ + delay = prototype->delay; + duration = prototype->duration; + animationGroup = prototype->animationGroup; + + this->fromValue = fromValue; + this->timingFunction = timingFunction; +} + +CABasicAnimation::CABasicAnimation(CABasicAnimation* animation) { + keyPath = animation->keyPath; + duration = animation->duration; + delay = animation->delay; + creationTime = animation->creationTime; + fillMode = animation->fillMode; + fromValue = animation->fromValue; + toValue = animation->toValue; + animationGroup = animation->animationGroup; + isRemovedOnCompletion = animation->isRemovedOnCompletion; + timingFunction = animation->timingFunction; +} + +bool CABasicAnimation::wasCreatedInUIAnimateBlock() { + return animationGroup != nullptr; +} + +float CABasicAnimation::progressFor(Timer currentTime) { + auto elapsedTimeMinusDelayInMs = float(currentTime - creationTime) - (delay * 1000); + + // prevents a division by zero when animating with duration: 0 + if (duration <= 0) { + auto animationHasStarted = (elapsedTimeMinusDelayInMs > 0); + return animationHasStarted ? 1.0f : 0.0f; + } + + auto durationInMs = duration * 1000; + auto linearProgress = std::fmax(0, std::fmin(1, elapsedTimeMinusDelayInMs / durationInMs)); + return ease(linearProgress); +} + +float CABasicAnimation::ease(float x) { + if (timingFunction) { return timingFunction->at(x); } + return x; +} diff --git a/Submodules/UIKit/lib/CABasicAnimationPrototype.cpp b/Submodules/UIKit/lib/CABasicAnimationPrototype.cpp new file mode 100644 index 0000000..a162975 --- /dev/null +++ b/Submodules/UIKit/lib/CABasicAnimationPrototype.cpp @@ -0,0 +1,20 @@ +#include +#include + +namespace NXKit { + +CABasicAnimationPrototype::CABasicAnimationPrototype(double duration, double delay, std::shared_ptr animationGroup): + duration(duration), delay(delay), animationGroup(animationGroup) +{ } + +std::shared_ptr CABasicAnimationPrototype::createAnimation(std::string keyPath, AnimatableProperty fromValue) { + return new_shared( + shared_from_this(), + keyPath, + fromValue, + CAMediaTimingFunction::timingFunctionFrom(animationGroup->options) + ); +} + +} + diff --git a/Submodules/UIKit/lib/CALayer.cpp b/Submodules/UIKit/lib/CALayer.cpp index ce94537..f01dfff 100644 --- a/Submodules/UIKit/lib/CALayer.cpp +++ b/Submodules/UIKit/lib/CALayer.cpp @@ -3,6 +3,9 @@ // #include "CALayer.h" +#include +#include +#include #include "include/core/SkRRect.h" #include "include/effects/SkGradientShader.h" @@ -11,9 +14,36 @@ using namespace NXKit; bool CALayer::layerTreeIsDirty = true; +NXFloat CALayer::defaultAnimationDuration = 0.3f; CALayer::CALayer() = default; +CALayer::CALayer(CALayer* layer) { + delegate = layer->delegate; + _bounds = layer->_bounds; + _transform = layer->_transform; + _position = layer->_position; + _anchorPoint = layer->_anchorPoint; + _opacity = layer->_opacity; + _backgroundColor = layer->_backgroundColor; + _isHidden = layer->_isHidden; + _cornerRadius = layer->_cornerRadius; +// borderWidth = layer->borderWidth; +// borderColor = layer->borderColor; +// shadowColor = layer->shadowColor; +// shadowPath = layer->shadowPath; +// shadowOffset = layer->shadowOffset; +// shadowRadius = layer->shadowRadius; +// shadowOpacity = layer->shadowOpacity; + _mask = layer->_mask; + _masksToBounds = layer->_masksToBounds; + _contents = layer->_contents; // XXX: we should make a copy here + _contentsScale = layer->_contentsScale; + _superlayer = layer->_superlayer; + _sublayers = layer->_sublayers; + _contentsGravity = layer->_contentsGravity; +} + void CALayer::setAnchorPoint(NXKit::NXPoint anchorPoint) { if (_anchorPoint == anchorPoint) return; onWillSet("anchorPoint"); @@ -213,7 +243,7 @@ void CALayer::skiaRender(SkCanvas* canvas) { // Origin matrix save 2 // restore canvas->restore(); for (const auto& sublayer: _sublayers) { - sublayer->skiaRender(canvas); + sublayer->presentationOrSelf()->skiaRender(canvas); } // Initial save 1 // restore @@ -256,17 +286,193 @@ void CALayer::setFrame(NXRect frame) { setBounds(bounds); } +CALayer* CALayer::copy() { + return new CALayer(this); +} + +std::shared_ptr CALayer::actionForKey(std::string event) { + if (!delegate.expired()) return delegate.lock()->actionForKey(event); + return CALayer::defaultActionForKey(event); +} + +std::shared_ptr CALayer::defaultActionForKey(std::string event) { + auto animation = new_shared(event); + animation->duration = CATransaction::animationDuration(); + return animation; +} + +std::shared_ptr CALayer::createPresentation() { + auto copy = new_shared(this); + copy->isPresentationForAnotherLayer = true; + return copy; +} + +// MARK: - Animations +void CALayer::add(std::shared_ptr animation, std::string keyPath) { + auto copy = new_shared(animation.get()); + copy->creationTime = Timer(); + + // animation.fromValue is optional, set it to currently visible state if nil + if (!copy->fromValue.has_value() && copy->keyPath.has_value()) { + auto presentation = _presentation; + if (!presentation) presentation = shared_from_this(); + + copy->fromValue = presentation->value(keyPath); + } + + if (copy->animationGroup) + copy->animationGroup->queuedAnimations += 1; + + if (animations.count(keyPath) && animations[keyPath]->animationGroup) + animations[keyPath]->animationGroup->animationDidStop(false); + + auto isEmpty = animations.empty(); + animations[keyPath] = copy; + onDidSetAnimations(isEmpty); +} + +void CALayer::removeAllAnimations() { + auto isEmpty = animations.empty(); + animations.clear(); + onDidSetAnimations(isEmpty); +} + +void CALayer::removeAnimation(std::string forKey) { + auto isEmpty = animations.empty(); + animations.erase(forKey); + onDidSetAnimations(isEmpty); +} + void CALayer::onWillSet(std::string keyPath) { CALayer::layerTreeIsDirty = true; auto animationKey = keyPath; -// auto animation = std::static_pointer_cast(actionForKey(animationKey)); -// if (animation && -// (this->hasBeenRenderedInThisPartOfOverallLayerHierarchy -// || animation->wasCreatedInUIAnimateBlock()) && -// !this->isPresentationForAnotherLayer && -// !CATransaction::disableActions()) -// { -// add(animation, animationKey); -// } + auto animation = std::static_pointer_cast(actionForKey(animationKey)); + if (animation && + (this->hasBeenRenderedInThisPartOfOverallLayerHierarchy + || animation->wasCreatedInUIAnimateBlock()) && + !this->isPresentationForAnotherLayer && + !CATransaction::disableActions()) + { + add(animation, animationKey); + } +} + +void CALayer::onDidSetAnimations(bool wasEmpty) { + if (!animations.empty() && wasEmpty) { + UIView::layersWithAnimations.insert(shared_from_this()); + _presentation = createPresentation(); + } else if (animations.empty() && !wasEmpty) { + _presentation = nullptr; + UIView::layersWithAnimations.erase(shared_from_this()); + } +} + +std::optional CALayer::value(std::string forKeyPath) { + if (forKeyPath == "backgroundColor") return _backgroundColor; + if (forKeyPath == "opacity") return _opacity; + if (forKeyPath == "bounds") return _bounds; + if (forKeyPath == "transform") return _transform; + if (forKeyPath == "position") return _position; + if (forKeyPath == "anchorPoint") return _anchorPoint; + if (forKeyPath == "cornerRadius") return _cornerRadius; + return std::nullopt; +} + +std::shared_ptr CALayer::presentationOrSelf() { + if (_presentation) return _presentation; + return shared_from_this(); +} + +void CALayer::animateAt(Timer currentTime) { + auto presentation = createPresentation(); + + auto animationsCopy = animations; + for (auto& animation: animationsCopy) { + auto animationProgress = animation.second->progressFor(currentTime); + update(presentation, animation.second, animationProgress); + + if (animationProgress == 1 && animation.second->isRemovedOnCompletion) { + removeAnimation(animation.first); + if (animation.second->animationGroup) + animation.second->animationGroup->animationDidStop(true); + } + } + + this->_presentation = animations.empty() ? nullptr : presentation; +} + +// Writing into `presentation->_...` cause we don't need onWillSet to be triggered +void CALayer::update(std::shared_ptr presentation, std::shared_ptr animation, float progress) { + if (!animation->keyPath.has_value() || !animation->fromValue.has_value()) return; + + auto keyPath = animation->keyPath.value(); + auto fromValue = animation->fromValue.value(); + + if (keyPath == "backgroundColor") { + auto start = any_optional_cast>(fromValue); + if (!start.has_value()) { return; } + + auto end = any_optional_cast>(animation->toValue); + if (!end.has_value()) end = this->_backgroundColor; + if (!end.has_value()) end = UIColor::clear; + + presentation->setBackgroundColor(start.value()->interpolationTo(end.value().value(), progress)); + } + if (keyPath == "position") { + auto start = any_optional_cast(fromValue); + if (!start.has_value()) { return; } + + auto end = any_optional_cast(animation->toValue); + if (!end.has_value()) end = this->_position; + + presentation->setPosition(start.value() + (end.value() - start.value()) * progress); + } + if (keyPath == "anchorPoint") { + auto start = any_optional_cast(fromValue); + if (!start.has_value()) { return; } + + auto end = any_optional_cast(animation->toValue); + if (!end.has_value()) end = this->_anchorPoint; + + presentation->setAnchorPoint(start.value() + (end.value() - start.value()) * progress); + } + if (keyPath == "bounds") { + auto start = any_optional_cast(fromValue); + if (!start.has_value()) { return; } + + auto end = any_optional_cast(animation->toValue); + if (!end.has_value()) end = this->_bounds; + + presentation->setBounds(start.value() + (end.value() - start.value()) * progress); + } + if (keyPath == "opacity") { + auto start = any_optional_cast(fromValue); + if (!start.has_value()) { return; } + + auto end = any_optional_cast(animation->toValue); + if (!end.has_value()) end = this->_opacity; + + presentation->setOpacity(start.value() + (end.value() - start.value()) * progress); + } + if (keyPath == "cornerRadius") { + auto start = any_optional_cast(fromValue); + if (!start.has_value()) { return; } + + auto end = any_optional_cast(animation->toValue); + if (!end.has_value()) end = this->_cornerRadius; + + presentation->setCornerRadius(start.value() + (end.value() - start.value()) * progress); + } + if (keyPath == "transform") { + auto start = any_optional_cast(fromValue); + if (!start.has_value()) { return; } + + auto end = any_optional_cast(animation->toValue); + if (!end.has_value()) end = this->_transform; + +// presentation->_transform = start.value() + (end.value() - start.value()) * progress; +// presentation->_transform = start.value() + (end.value() - start.value()).interpolate(progress); + presentation->setTransform(start.value().interpolateTo(end.value(), progress)); + } } diff --git a/Submodules/UIKit/lib/CAMediaTimingFunction.cpp b/Submodules/UIKit/lib/CAMediaTimingFunction.cpp new file mode 100644 index 0000000..a8cfbd9 --- /dev/null +++ b/Submodules/UIKit/lib/CAMediaTimingFunction.cpp @@ -0,0 +1,81 @@ +#include +#include +#include + +namespace NXKit { + +double CAMediaTimingFunction::linear(double x) { return x; } +double CAMediaTimingFunction::easeInCubic(double x) { return std::pow(x, 3); } +double CAMediaTimingFunction::easeOutCubic(double x) { + auto t = x - 1; + return ((t * t * t) + 1); +} +double CAMediaTimingFunction::easeInQuad(double x) { return pow(x, 2); } +double CAMediaTimingFunction::easeOutQuad(double x) { return x * (2 - x); } +double CAMediaTimingFunction::easeInOutCubic(double x) { + return x < 0.5 ? 2 * (x * x) : -1 + (4 - 2 * x) * x; +} +double CAMediaTimingFunction::customEaseOut(double x) { + auto term1 = UIScrollViewDecelerationRateNormal * 3 * x * pow(1 - x, 2); + auto term2 = 3 * (x * x) * (1 - x); + auto term3 = x * x * x; + + return term1 + term2 + term3; +} +double CAMediaTimingFunction::easeOutElastic(double x) { + auto c4 = (2 * M_PI) / 3; + + return x == 0 + ? 0 + : x == 1 + ? 1 + : pow(2, -10 * x) * sin((x * 10 - 0.75) * c4) + 1; +} + +CAMediaTimingFunction::CAMediaTimingFunction(std::string name) { + if (name == kCAMediaTimingFunctionDefault) + timing = CAMediaTimingFunction::easeOutCubic; + else if (name == kCAMediaTimingFunctionLinear) + timing = CAMediaTimingFunction::linear; + else if (name == kCAMediaTimingFunctionEaseIn) + timing = CAMediaTimingFunction::easeInCubic; + else if (name == kCAMediaTimingFunctionEaseOut) + timing = CAMediaTimingFunction::easeOutQuad; + else if (name == kCAMediaTimingFunctionCustomEaseOut) + timing = CAMediaTimingFunction::customEaseOut; + else if (name == kCAMediaTimingFunctionEaseInEaseOut) + timing = CAMediaTimingFunction::easeInOutCubic; + else if (name == kCAMediaTimingFunctionEaseOutElastic) + timing = CAMediaTimingFunction::easeOutElastic; + else + throw -1; +// fatalError("invalid name in CAMediaTimingFunction init"); +} + +CAMediaTimingFunction::CAMediaTimingFunction(std::function timing) { + this->timing = timing; +} + +std::shared_ptr CAMediaTimingFunction::timingFunctionFrom(UIViewAnimationOptions options) { + if ((options & UIViewAnimationOptions::curveEaseIn) == UIViewAnimationOptions::curveEaseIn) { + return new_shared(kCAMediaTimingFunctionEaseIn); + } else if ((options & UIViewAnimationOptions::curveEaseOut) == UIViewAnimationOptions::curveEaseOut) { + return new_shared(kCAMediaTimingFunctionEaseOut); + } else if ((options & UIViewAnimationOptions::curveEaseInOut) == UIViewAnimationOptions::curveEaseInOut) { + return new_shared(kCAMediaTimingFunctionEaseInEaseOut); + } else if ((options & UIViewAnimationOptions::customEaseOut) == UIViewAnimationOptions::customEaseOut) { + return new_shared(kCAMediaTimingFunctionCustomEaseOut); + } else if ((options & UIViewAnimationOptions::curveLinear) == UIViewAnimationOptions::curveLinear) { + return new_shared(kCAMediaTimingFunctionLinear); + } else if ((options & UIViewAnimationOptions::curveEaseOutElastic) == UIViewAnimationOptions::curveEaseOutElastic) { + return new_shared(kCAMediaTimingFunctionEaseOutElastic); + } + + return new_shared(kCAMediaTimingFunctionDefault); +} +float CAMediaTimingFunction::at(float x) { + return timing(x); +} + +} + diff --git a/Submodules/UIKit/lib/CASpringAnimation.cpp b/Submodules/UIKit/lib/CASpringAnimation.cpp new file mode 100644 index 0000000..7fd391a --- /dev/null +++ b/Submodules/UIKit/lib/CASpringAnimation.cpp @@ -0,0 +1,19 @@ +#include + +namespace NXKit { + +CASpringAnimation::CASpringAnimation(CASpringAnimation* animation): + CABasicAnimation(animation), + damping(animation->damping), + initialSpringVelocity(animation->initialSpringVelocity) +{ } + +CASpringAnimation::CASpringAnimation(std::shared_ptr prototype, + std::string keyPath, + AnimatableProperty fromValue): + CABasicAnimation(prototype, keyPath, fromValue, new_shared(CAMediaTimingFunction::easeOutElastic)), + damping(prototype->damping), + initialSpringVelocity(prototype->initialSpringVelocity) +{ } + +} diff --git a/Submodules/UIKit/lib/CASpringAnimationPrototype.cpp b/Submodules/UIKit/lib/CASpringAnimationPrototype.cpp new file mode 100644 index 0000000..2066999 --- /dev/null +++ b/Submodules/UIKit/lib/CASpringAnimationPrototype.cpp @@ -0,0 +1,21 @@ +#include +#include + +namespace NXKit { + +CASpringAnimationPrototype::CASpringAnimationPrototype(double duration, + double delay, + double damping, + double initialSpringVelocity, + std::shared_ptr animationGroup) : + CABasicAnimationPrototype(duration, delay, animationGroup), + damping(damping), + initialSpringVelocity(initialSpringVelocity) +{ } + +std::shared_ptr CASpringAnimationPrototype::createAnimation(std::string keyPath, AnimatableProperty fromValue) { + auto self = std::static_pointer_cast(shared_from_this()); + return new_shared(self, keyPath, fromValue); +} + +} diff --git a/Submodules/UIKit/lib/CATransaction.cpp b/Submodules/UIKit/lib/CATransaction.cpp new file mode 100644 index 0000000..76b31db --- /dev/null +++ b/Submodules/UIKit/lib/CATransaction.cpp @@ -0,0 +1,35 @@ +#include + +namespace NXKit { + +std::vector CATransaction::transactionStack; + +void CATransaction::begin() { + transactionStack.push_back(CATransaction()); +} + +void CATransaction::commit() { + transactionStack.pop_back(); +} + +bool CATransaction::disableActions() { + if (transactionStack.empty()) return false; + return transactionStack.back().disableActions(); +} + +void CATransaction::setDisableActions(bool newValue) { + if (transactionStack.empty()) { return; } + transactionStack[transactionStack.size() - 1].disableActions_ = newValue; +} + +float CATransaction::animationDuration() { + if (transactionStack.empty()) return CALayer::defaultAnimationDuration; + return transactionStack.back().animationDuration_; +} + +void CATransaction::setAnimationDuration(float newValue) { + if (transactionStack.empty()) { return; } + transactionStack[transactionStack.size() - 1].animationDuration_ = newValue; +} + +} diff --git a/Submodules/UIKit/lib/UIApplicationMain.cpp b/Submodules/UIKit/lib/UIApplicationMain.cpp index bd51d0b..1df6a75 100644 --- a/Submodules/UIKit/lib/UIApplicationMain.cpp +++ b/Submodules/UIKit/lib/UIApplicationMain.cpp @@ -15,6 +15,8 @@ bool applicationRunLoop() { // UIRenderer::main()->render(UIApplication::shared->keyWindow.lock(), currentTime); + UIView::animateIfNeeded(currentTime); + // Move to UIRenderer auto surface = SkiaCtx::_main->getBackbufferSurface(); @@ -29,7 +31,7 @@ bool applicationRunLoop() { auto keyWindow = UIApplication::shared->keyWindow.lock(); keyWindow->layer()->setBounds({ NXPoint::zero, SkiaCtx::_main->getSize() } ); - keyWindow->layer()->skiaRender(canvas); + keyWindow->layer()->presentationOrSelf()->skiaRender(canvas); canvas->restore(); SkiaCtx::_main->flushAndSubmit(surface); diff --git a/Submodules/UIKit/lib/UIColor.cpp b/Submodules/UIKit/lib/UIColor.cpp index ace8bb7..6cf6297 100644 --- a/Submodules/UIKit/lib/UIColor.cpp +++ b/Submodules/UIKit/lib/UIColor.cpp @@ -1,4 +1,6 @@ #include "UIColor.h" +#include +#include using namespace NXKit; @@ -44,3 +46,32 @@ unsigned char UIColor::a() { bool UIColor::operator==(const UIColor& rhs) const { return this->color == rhs.color; } + +UIColor UIColor::interpolationTo(UIColor endResult, NXFloat progress) { + auto startR = r(); + auto startG = g(); + auto startB = b(); + auto startA = a(); + + auto endR = endResult.r(); + auto endG = endResult.g(); + auto endB = endResult.b(); + auto endA = endResult.a(); + + auto currentProgress = progress * 255; + auto maxProgress = UINT8_MAX; + + auto resultR = startR + (endR - startR) * currentProgress / maxProgress; + auto resultG = startG + (endG - startG) * currentProgress / maxProgress; + auto resultB = startB + (endB - startB) * currentProgress / maxProgress; + auto resultA = startA + (endA - startA) * currentProgress / maxProgress; + +#define res(x) fmaxf(0, fminf(255, abs(x))) + + return UIColor( + res(resultR), + res(resultG), + res(resultB), + res(resultA) + ); +} diff --git a/Submodules/UIKit/lib/UIView.cpp b/Submodules/UIKit/lib/UIView.cpp index 7712555..9975b70 100644 --- a/Submodules/UIKit/lib/UIView.cpp +++ b/Submodules/UIKit/lib/UIView.cpp @@ -1,4 +1,6 @@ #include +#include +#include using namespace NXKit; @@ -9,6 +11,7 @@ std::shared_ptr UIView::initLayer() { UIView::UIView(NXRect frame) { _layer = initLayer(); _layer->setAnchorPoint(NXPoint::zero); + _layer->delegate = weak_from_this(); setFrame(frame); } @@ -148,3 +151,87 @@ void UIView::setContentMode(UIViewContentMode mode) { break; } } + +// MARK: - Animations +std::set> UIView::layersWithAnimations; +std::shared_ptr UIView::currentAnimationPrototype; + +bool UIView::anyCurrentlyRunningAnimationsAllowUserInteraction() { + if (layer()->animations.empty()) return true; + + for (auto& animation: layer()->animations) { + auto animationGroup = animation.second->animationGroup; + if (animationGroup && (animationGroup->options & UIViewAnimationOptions::allowUserInteraction) == UIViewAnimationOptions::allowUserInteraction) { + return true; + } + } + + return false; +} + +void UIView::animate(double duration, double delay, UIViewAnimationOptions options, std::function animations, std::function completion) { + auto group = new_shared(options, completion); + currentAnimationPrototype = new_shared(duration, delay, group); + + animations(); + + if (currentAnimationPrototype && currentAnimationPrototype->animationGroup->queuedAnimations == 0) { + DispatchQueue::main()->async([completion]() { completion(true); }); + } + + currentAnimationPrototype = nullptr; +} + + +void UIView::animate(double duration, std::function animations) { + UIView::animate( duration, 0, UIViewAnimationOptions::none, animations); +} + +void UIView::animate(double duration, double delay, double damping, double initialSpringVelocity, UIViewAnimationOptions options, std::function animations, std::function completion) { + auto group = new_shared(options, completion); + currentAnimationPrototype = new_shared( duration, delay, damping, initialSpringVelocity, group); + + animations(); + + if (currentAnimationPrototype && currentAnimationPrototype->animationGroup->queuedAnimations == 0) { + completion(true); + } + currentAnimationPrototype = nullptr; +} + +void UIView::animateIfNeeded(Timer currentTime) { + auto layersWithAnimationsCopy = layersWithAnimations; + for (auto& layer: layersWithAnimationsCopy) { + layer->animateAt(currentTime); + } +} + +void UIView::completePendingAnimations() { + for (auto& layer: layersWithAnimations) { + timeval now; + gettimeofday(&now, nullptr); + // FIXME: incorrect logic + layer->animateAt(Timer(timevalInMilliseconds(now) + 1000000000)); + // $0.animate(at: Timer(startingAt: NSDate.distantFuture.timeIntervalSinceNow)); + } +} + +std::shared_ptr UIView::actionForKey(std::string event) { + auto prototype = UIView::currentAnimationPrototype; + if (!prototype) { return nullptr; } + + auto keyPath = event; + auto beginFromCurrentState = (prototype->animationGroup->options & UIViewAnimationOptions::beginFromCurrentState) == UIViewAnimationOptions::beginFromCurrentState; + + auto state = beginFromCurrentState ? (_layer->presentationOrSelf()) : _layer; + + auto fromValue = state->value(keyPath); + + if (fromValue.has_value()) { + return prototype->createAnimation(keyPath, fromValue.value()); + } + + return nullptr; +} + +void UIView::display(std::shared_ptr layer) { } diff --git a/Submodules/UIKit/lib/UIViewAnimationGroup.cpp b/Submodules/UIKit/lib/UIViewAnimationGroup.cpp new file mode 100644 index 0000000..031a5c2 --- /dev/null +++ b/Submodules/UIKit/lib/UIViewAnimationGroup.cpp @@ -0,0 +1,17 @@ +#include + +namespace NXKit { + +UIViewAnimationGroup::UIViewAnimationGroup(UIViewAnimationOptions options, std::optional> completion): + options(options), completion(completion) +{ } + +void UIViewAnimationGroup::animationDidStop(bool finished) { + queuedAnimations -= 1; + if (queuedAnimations == 0) { + if (completion.has_value()) + completion.value()(finished); + } +} + +} diff --git a/Submodules/UIKit/lib/UIViewAnimationOptions.cpp b/Submodules/UIKit/lib/UIViewAnimationOptions.cpp new file mode 100644 index 0000000..e69de29 diff --git a/app/AppDelegate.cpp b/app/AppDelegate.cpp index 291ca68..d5c96b6 100644 --- a/app/AppDelegate.cpp +++ b/app/AppDelegate.cpp @@ -37,10 +37,14 @@ bool UIApplicationDelegate::applicationDidFinishLaunchingWithOptions(UIApplicati imageView->setFrame({ 0, 0, 120, 120 }); imageView->setBackgroundColor(UIColor::green); imageView->setContentMode(UIViewContentMode::topLeft); - imageView->setTransform(NXAffineTransform::identity.rotationBy(45)); - + subview->addSubview(imageView); + UIView::animate(2, [imageView]() { + imageView->setTransform(NXAffineTransform::identity.rotationBy(45)); + }); + + return true; }