Skip to content

Instantly share code, notes, and snippets.

@IainS1986
Created April 6, 2021 13:24
Show Gist options
  • Save IainS1986/39de43aadf7f32d2118cb8d1e4aaa716 to your computer and use it in GitHub Desktop.
Save IainS1986/39de43aadf7f32d2118cb8d1e4aaa716 to your computer and use it in GitHub Desktop.
iOS Timing Function lookup to be able to get any value from within a given iOS timing curve (EaseIn, EaseOut, EaseInOut etc). This is itself a port of a port. Original logic found in RSTimingFunction (https://gist.github.com/raphaelschaad/6739676) and the Swift 4.2 port (https://gist.github.com/tcldr/204c4bc87c5239a53239a46728214715)
using System;
using CoreGraphics;
using UIKit;
// Xamarin port of the swift port of https://gist.github.com/raphaelschaad/6739676
// Swift 4.2 port https://gist.github.com/tcldr/204c4bc87c5239a53239a46728214715
//
// Basic Usage:
// var timingFunction = new TimingFunction(new UICubicTimingParameters(UIViewAnimationCurve.EaseInOut));
// var progress = timingFunction.Progress(time); // where time is 0->duration, use 0-1 for easier normalised look ups
public struct TimingFunction
{
private CGPoint _controlPoint1;
private CGPoint _controlPoint2;
private UnitBezier _unitBezier;
private float _epsilon;
public TimingFunction(UICubicTimingParameters timingParameters, float duration = 1)
{
_controlPoint1 = timingParameters.ControlPoint1;
_controlPoint2 = timingParameters.ControlPoint2;
_unitBezier = new UnitBezier(_controlPoint1, _controlPoint2);
_epsilon = 1.0f / (200.0f * duration);
}
public TimingFunction(CGPoint controlPoint1, CGPoint controlPoint2, float duration = 1)
{
_controlPoint1 = controlPoint1;
_controlPoint2 = controlPoint2;
_unitBezier = new UnitBezier(_controlPoint1, _controlPoint2);
_epsilon = 1.0f / (200.0f * duration);
}
/// Returns the progress along the timing function for the given time (`fractionComplete`)
/// with `0.0` equal to the start of the curve, and `1.0` equal to the end of the curve
public float Progress(float fractionComplete)
=> _unitBezier.Value(fractionComplete, _epsilon);
}
public struct UnitBezier
{
private float _ax;
private float _bx;
private float _cx;
private float _ay;
private float _by;
private float _cy;
public UnitBezier(CGPoint controlPoint1, CGPoint controlPoint2)
{
// Calculate the polynomial coefficients, implicit first
// and last control points are (0,0) and (1,1).
_cx = (float)(3.0f * controlPoint1.X);
_bx = (float)(3.0f * (controlPoint2.X - controlPoint1.X) - _cx);
_ax = 1.0f - _cx - _bx;
_cy = (float)(3.0f * controlPoint1.Y);
_by = (float)(3.0f * (controlPoint2.Y - controlPoint1.Y) - _cy);
_ay = 1.0f - _cy - _by;
}
public float Value(float x, float epsilon)
=> SampleCurveY(SolveCurveX(x, epsilon));
// `ax t^3 + bx t^2 + cx t' expanded using Horner's rule.
private float SampleCurveX(float t)
=> ((_ax * t + _bx) * t + _cx) * t;
private float SampleCurveY(float t)
=> ((_ay * t + _by) * t + _cy) * t;
private float SampleCurveDerivativeX(float t)
=> (3.0f * _ax * t + 2.0f * _bx) * t + _cx;
private float SolveCurveX(float x, float epsilon)
{
float t0, t1, t2, x2, d2;
// First try a few iterations of Newton's method -- normally very fast.
t2 = x;
for(int i=0; i<8; i++)
{
x2 = SampleCurveX(t2) - x;
if (Math.Abs(x2) < epsilon)
return t2;
d2 = SampleCurveDerivativeX(t2);
if (Math.Abs(d2) < 0.00000001f)
break;
t2 = t2 - x2 / d2;
}
// Fall back to the bisection method for reliability.
t0 = 0.0f;
t1 = 1.0f;
t2 = x;
if (t2 < t0)
return t0;
if (t2 > t1)
return t1;
while (t0 < t1)
{
x2 = SampleCurveX(t2);
if (Math.Abs(x2 - x) < epsilon)
return t2;
if (x > x2)
t0 = t2;
else
t1 = t2;
t2 = t0 + ((t1 - t0) / 2);
}
// Failure
return t2;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment