Git Product home page Git Product logo

wave's Introduction

Wave

Wave is a spring-based animation engine for iOS, iPadOS, and macOS. It makes it easy to create fluid, interactive, and interruptible animations that feel great.

Wave has no external dependencies, and can be easily dropped into existing UIKit, SwiftUI, or AppKit based projects and apps.

The core feature of Wave is that all animations are re-targetable, meaning that you can change an animation’s destination value in-flight, and the animation will gracefully redirect to that new value.

Understanding Retargeting

Consider these demos of the iOS Picture-in-Picture feature. The screen on the left is created with standard UIKit animations, and the one on the right is created with Wave.

Though both are “interruptible”, the Wave-based implementation handles the interruption much better, and fluidly arcs to its new destination. The UIKit animation feels stiff and jerky in comparison.

At its core, retargeting is the process of preserving an animation’s velocity even as its target changes, which Wave does automatically.

Demo

Installation

Add Wave to your app's Package.swift file, or selecting File -> Add Packages in Xcode:

.package(url: "https://github.com/jtrivedi/Wave")

If you clone the repo, you can run the sample app, which contains a few interactive demos to understand what Wave provides.

Note: To enable high frame-rate animations on ProMotion devices (i.e. 120 fps animation), you'll need to add a key/value pair in your Info.plist. Set the key CADisableMinimumFrameDuration to true. Without this entry, animations will be capped at 60 fps.

Documentation

There’s a full Wave documentation site available for full API and usage documentation.

Getting Started

There are two ways you can interact with Wave, depending on your needs: the block-based and property-based animations:

Block-Based Animation

The easiest way to get started is by using Wave’s block-based APIs that resemble the UIView.animateWithDuration() APIs.

This API lets you animate several common UIView and CALayer properties, like frame, center, scale, backgroundColor, and more.

For these supported properties, Wave will create, manage, and execute the required spring animations under-the-hood.

For example, animating the above PiP view to its final destination is extremely simple:

if panGestureRecognizer.state == .ended {

    // Create a spring with some bounciness. `response` affects the animation's duration.
    let animatedSpring = Spring(dampingRatio: 0.68, response: 0.80)

    // Get the gesture's lift-off velocity, and pass it into the Wave animation.
    let gestureVelocity = panGestureRecognizer.velocity(in: view)

    Wave.animate(withSpring: animatedSpring, gestureVelocity: gestureVelocity) {
        // Update animatable properties on the view's `animator` property, _not_ the view itself.
        pipView.animator.center = pipViewDestination     // Some target CGPoint that you calculate.
        pipView.animator.scale = CGPoint(x: 1.1, y: 1.1)
    }
}

Note that at any time, you can retarget the view’s center property to somewhere else, and it’ll gracefully animate.

Supported Animatable Properties

The block-based API currently supports animating the following properties. For other properties, you can use the property-based animation API below.

  • frame
  • bounds
  • center
  • origin
  • alpha
  • backgroundColor
  • cornerRadius
  • scale
  • translation
  • shadowColor/radius/offset/opacity
  • borderColor/borderWidth

Upcoming properties:

  • rotation

Property-Based Animation

While the block-based API is often most convenient, you may want to animate something that the block-based API doesn’t yet support (e.x. rotation). Or, you may want the flexibility of getting the intermediate spring values and driving an animation yourself (e.x. a progress value).

For example, to draw the orange path of the PiP demo, we need to know the value of every CGPoint from the view’s initial center, to its destination center:

// When the gesture ends, create a `CGPoint` animator from the PiP view's initial center, to its target.
// The `valueChanged` callback provides the intermediate locations of the callback, allowing us to draw the path.

let positionAnimator = SpringAnimator<CGPoint>(spring: animatedSpring)
positionAnimator.value = pipView.center       // The presentation value
positionAnimator.target = pipViewDestination  // The target value
positionAnimator.velocity = gestureVelocity

positionAnimator.valueChanged = { [weak self] location in
    self?.drawPathPoint(at: location)
}

positionAnimator.start()
Completion Blocks

Both the block-based and property-based APIs support completion blocks. If an animation completes fully, the completion block’s finished flag will be true.

However, if an animation’s target was changed in-flight (“retargeted”), finished will be false, while retargeted will be true.

Wave.animate(withSpring: Spring.defaultAnimated) {
    myView.animator.backgroundColor = .systemBlue
} completion: { finished, retargeted in
    print(finished, retargeted)
}

Example Code

Exploring the provided sample app is a great way to get started with Wave.

Simply open the Wave-Sample Xcode project and hit “Run”. The full source code for the Picture-in-Picture demo is available there, too!

Acknowledgements

Special thanks to Ben Oztalay for helping architect the underlying physics of Wave!

wave's People

Contributors

jtrivedi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

wave's Issues

Animation broken on UIViewControllerAnimatedTransitioning

Hi jtrivedi,
Thanks for sharing. I find some trouble when using Wave with UIViewControllerAnimatedTransitioning
This code works fine with UIView.animation.

import UIKit
import Wave

class PopupViewController: UIViewController {
    lazy var contentView: UIView = {
        let view = UIView()
        view.backgroundColor = .white
        view.layer.cornerRadius = 10
        view.layer.masksToBounds = true
        view.alpha = 0
        view.transform = .init(scaleX: 0, y: 0)
        return view
    }()

    lazy var blurEffectView: UIVisualEffectView = {
        let blurEffect = UIBlurEffect(style: .dark)
        let blurEffectView = UIVisualEffectView(effect: blurEffect)
        blurEffectView.frame = view.bounds
        blurEffectView.alpha = 0.0
        return blurEffectView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
    }

    @objc func viewTapped() {
        dismiss(animated: true)
    }

    private func setupViews() {
        view.backgroundColor = .clear

        blurEffectView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(viewTapped)))

        view.addSubview(blurEffectView)

        view.addSubview(contentView)
        contentView.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.leading.equalToSuperview().offset(20)
            make.trailing.equalToSuperview().offset(-20)
            make.height.equalTo(400)
        }
    }
}

final class TransitionManager:
    NSObject,
    UIViewControllerTransitioningDelegate,
    UIViewControllerAnimatedTransitioning
{
    private let transitionDuration: TimeInterval = 0.6

    let interactiveTransition = UIPercentDrivenInteractiveTransition()

    func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval {
        transitionDuration
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView
        if let toViewController = transitionContext.viewController(forKey: .to) as? PopupViewController {
            containerView.addSubview(toViewController.view)
            toViewController.contentView.center.y -= containerView.bounds.size.height
            
            let animatedSpring = Spring(dampingRatio: 0.68, response: transitionDuration)
            Wave.animate(withSpring: animatedSpring) {
                toViewController.blurEffectView.alpha = 1
                toViewController.contentView.alpha = 1
                toViewController.contentView.transform = .identity
                toViewController.contentView.center.y += containerView.bounds.size.height
            } completion: { _,_  in
                transitionContext.completeTransition(true)
            }

        } else if let fromViewController = transitionContext.viewController(forKey: .from) as? PopupViewController {
            containerView.addSubview(fromViewController.view)
            let animatedSpring = Spring(dampingRatio: 0.68, response: transitionDuration)
            Wave.animate(withSpring: animatedSpring) {
                fromViewController.blurEffectView.alpha = 0
                fromViewController.contentView.center.y -= containerView.bounds.size.height
                fromViewController.contentView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
            } completion: { _,_  in
                transitionContext.completeTransition(true)
            }
        }
    }

    func animationController(forPresented _: UIViewController, presenting _: UIViewController, source _: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return self
    }

    func animationController(forDismissed _: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return self
    }
}
class ViewController: UIViewController {
    ...
    ...
    private let transition = TransitionManager()
    let viewController = PopupViewController()
    viewController.modalPresentationStyle = .custom
    viewController.transitioningDelegate = transition
    present(viewController, animated: true, completion: nil)
    ...
    ...
}

DisplayLinkProvider preferredFrameRateRange range error

Hi,

First of all - great library! I am trying this out with SwiftUI (although I think this issue is unrelated) but I am encountering an issue. On my physical device (iPhone 11 Pro), the returned value of UIScreen.main.maximumFramesPerSecond is 61.

This makes the following error out:

        if #available(iOS 15.0, *) {
            let maximumFramesPerSecond = Float(UIScreen.main.maximumFramesPerSecond)
            let highFPSEnabled = maximumFramesPerSecond > 60
            let minimumFPS: Float = highFPSEnabled ? 80 : 60
            displayLinkProvider?.preferredFrameRateRange = .init(minimum: minimumFPS, maximum: maximumFramesPerSecond, preferred: maximumFramesPerSecond)
        }

Error:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'invalid range (minimum: 80.00 maximum: 61.00 preferred: 61.00)'

For now, I am forking the repo to increase the highFPSEnabled threshold and see if that works. Is there any other way to make the check work more reliably?

Animation type names conflict with SwiftUI

Animation conflicts with SwiftUI.Animation, and Wave.Animation doesn't resolve correctly, since Wave is also a type name within the module.

We should probably rename Animation to SpringAnimator or something.

Support for non-spring animations?

This is an awesome library. Thanks for building it.

I was wondering - Do you have any plans for supporting non-spring animations (i.e. ease-in-out or linear)? Or are they already there and I missed it?

Thanks.

Animators in SwiftUI

Really exciting that we're able to use Wave in SwiftUI. Couple of questions:

Unexpected animator state in completion block

Hi 👋

The state of an animator is being updated after calling its completion block, resulting in being incorrect in said completion block.
This is major issue when you want to share a completion block with multiple animators with different settling times, and execute some logic after all animators have completed.

For example:

let firstAnimator = SpringAnimator<CGFloat>(...)
let secondAnimator = SpringAnimator<CGRect>(...)

let sharedCompletion = {
    guard case .ended = firstAnimator.state, case .ended = secondAnimator.state  // Will never be satisfied
    else { return }
    print("Both animations are finished.")
}

firstAnimator.completion = { completion in
    guard case .finished = completion else { return }
    // (firstAnimator.state == .ended) → false 😭
    sharedCompletion()
}

secondAnimator.completion = { completion in
    guard case .finished = completion else { return }
    // (secondAnimator.state == .ended) → false 😭
    sharedCompletion()
}

I understand that you don't accept contributions yet so I haven't opened a pull request, but if you change your mind, I don't mind opening one!
I forked the project and fixed the issue in the meantime.

Thanks again for this delightful animation engine, I love it!

Improve `backgroundColor` animation retargeting

The current implementation of animating backgroundColor with the block-based API really isn't correct. We need to fully decompose the initial and target color values into their RGBA components, and animate each one on their own spring. That way, if you retarget a color animation, the carryover velocities will actually be correct.

Allow an animation not to require an explicit restart after the target is reached then updated again.

This framework is great and I found it easy to replace Facebook pop – thank you!

One minor issue I have is that animations automatically stop when the target value is reached, then become inactive. Any further change to the target value is ignored until the animation is explicitly restarted. But an app may not care whether the animation has reached its target and may just want to animate changes by updating the target.

Facebook pop has a removedOnCompletion property to address this and keep animations alive when set to false. Would it be possible to add something similar?

HSL color space interpolation for UIColors

Optionally using HSL for color interpolation will provide nicer visual results when blending between colors, as compared to RGB. Specifically, this will help reduce muddy mid-interpolation hues.

Major wave update

I created a major wave update.. I never submitted a request, so please help me with it.

Extended animation support

  • NSView/UIView:
    • frame, size, origin, center, alpha, backgroundColor, borderColor, borderWidth, shadowColor, shadowOpacity, shadowOffset, shadowRadius, transform, scale, rotation, translation, cornerRadius
  • CALayer
    • frame, bounds, size, origin, center, opacity, backgroundColor, borderColor, borderWidth, shadowColor, shadowOpacity, shadowOffset, shadowRadius, transform, scale, rotation, translation, cornerRadius
  • NSTextField/UITextField, UILabel, UITextView:
    • fontSize, textColor
  • NSScrollView/UIScrollView:
    • contentOffset, zoomFactor
  • NSWindow:
    • frame, size, alpha, backgroundColor
  • NSLayoutConstraint:
    • constant

Spring

  • Feature, properties and functions parity to SwiftUI's Spring.
  • smooth, bouncy, snappy presets
  • Spring(duration: CGFloat, bounce: CGFloat)

SpringAnimator

  • Now uses AnimatableData instead of SpringInterpolatable.
  • Double, Float, CGFloat, CGPoint, CGSize, CGRect, CATransform3D, WaveColor, CGColor, CGAffineTransform support AnimatableData by default.
public protocol AnimatableData: Equatable, Comparable {
    /// The type defining the data to animate.
    associatedtype AnimatableData: VectorArithmetic = Self
    /// The data to animate.
    var animatableData: AnimatableData { get }
    /// Initializes with animatable data.
    init(_ animatableData: AnimatableData)
    /// Scaled integral representation of the value.
    var scaledIntegral: Self { get }
    static var zero: Self { get }
}

##AnimatablePropertyProvider protocol
Extending a class with AnimatablePropertyProvider adds a animator: PropertyAnimator<Class> property.

To set/get a property animated use the keyPath on the animator.

extension NSView: AnimatablePropertyProvider { }

Wave.animate(withSpring: .bouncy) {
        myView.animator[\.frame] = newFrame // sets a new frame animated.
        let currentFrame = myView.animator[\.frame] // current frame (either the target of the spring animation or the frame)
}

This allows modularity. E.g.

extension NSView: AnimatablePropertyProvider { }

extension PropertyAnimator<NSView> {
    var frame: CGRect {
        get { self[\.frame] }
        set { self[\.frame] = newValue }
    }
}

myView.animator.frame = newFrame

extension CALayer: AnimatablePropertyProvider { }

extension PropertyAnimator<CALayer> {
    var opacity: CGFloat {
        get { self[\.opacity] }
        set { self[\.opacity] = newValue }
    }
}

myLayer.animator.opacity = 0.5

macOS version

I don't know how feasible this would be as you use a lot of UIKit, but what about some level of AppKit support? I don't mean Catalyst support as I imagine that may work out of the box.

Maybe next week Apple will announce something to better bridge the gap.

If this is something you don't ever want to support, feel free to close this. Awesome work!

More SwiftUI uses?

This isn't an issue just a request. Is it possible to recreate the AppKit demo in SwiftUI? I've been unsuccessful in my attempts, not sure if this is possible yet? Adding more SwiftUI examples would be greatly appreciated. I'd really enjoy adding this to my application to give it the pizazz it needs but haven't successfully found a use just yet.

Non-animated updates shouldn't wait until the next turn of the run loop

let v = UIView()
v.bounds.size = CGSize(width: 50, height: 50)
v.animator.scale = CGPoint(x: 0.5, y: 0.5)

print(v.frame.size)

This will print (50, 50) instead of (25, 25) until the next turn of the run loop. We shouldn't require the display link to fire to update things non-animatedly.

Should ubiquitous extensions be made public?

Right now, the library ships with a few public extensions of built-in types that are probably already implemented by clients. For example + and - operators on CGPoint. These extensions will compete with any in-house extension a client defines itself in a standalone helper library, and could cause friction at best, or undefined behaviors at worse.

Of course, these extensions are very nice, but is this the purpose of this library to provide such functionality? If these extensions are not required for the public API of the library (I haven't experimented with it yet), I suppose they should be kept internal.

Animation must have a non-nil `value` before starting.

I get this error when I try to use this in swiftui: "Animation must have a non-nil value before starting."
my code is here:

`.onAppear {
offsetAnimator.value = .zero

        // The offset animator's callback will update the `offset` state variable.
        offsetAnimator.valueChanged = { newValue in
            boxOffset = newValue
        }
    }
    .offset(x: boxOffset.x, y: boxOffset.y)
    .gesture(
        DragGesture()
            .onChanged { value in
                // Update the animator's target to the new drag translation.
                offsetAnimator.target = CGPoint(x: value.translation.width, y: value.translation.height)

                // Don't animate the box's position when we're dragging it.
                offsetAnimator.mode = .nonAnimated
                offsetAnimator.start()
            }
            .onEnded { value in
                // Animate the box to its original location (i.e. with zero translation).
                offsetAnimator.target = .zero

                // We want the box to animate to its original location, so use an `animated` mode.
                // This is different than the
                offsetAnimator.mode = .animated

                // Take the velocity of the gesture, and give it to the animator.
                // This makes the throw animation feel natural and continuous.
                offsetAnimator.velocity = CGPoint(x: value.velocity.width, y: value.velocity.height)
                offsetAnimator.start()
            }
    )`

thank you for your help

Animation delays should be interruptible

Wave animation delays shouldn't use dispatch_after. Instead, the engine should track delays internally so an animation's delay can be modified after the fact.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.