0

I am trying to create a custom animation that has the same animation curve as if I were using a CAMediaTimingFunction (I am doing the animation in OpenGL, so I am unable to use this). I would also like to stay away from UIKit in general as I would like it to be as multi-platform as possible.

CAMediaTimingFunction uses a cubic Bezier function to calculate the animation progress from the time that has elapsed. I have no idea how it does it though.

From what I understand, cubic Bezier functions are defined parametrically and it turns messy when you try to derive a cartesian equation from them.

To be clear: I want a method that takes an x-value input and returns a y-value output along a cubic Bezier curve. The control points would be limited from (0, 0) to (1, 1).

What I was planning on doing is generating a locus of points (say 100) for the bezier curve using the parametric equation in the following function twice for each point (once for x, once for y), using 100 values of t from 0 to 1

static inline CGFloat bezierFunc1D(CGFloat t, CGFloat p0, CGFloat p1, CGFloat p2, CGFloat p3) {
    return
    (1-t)*(1-t)*(1-t) * p0
    + 3 * (1-t)*(1-t) * t * p1
    + 3 * (1-t) * t*t * p2
    + t*t*t * p3;
}

This is where p0 through to p3 are the control points and t is the parametric scalar from 0 to 1. I could optimise this fairly easily by pre-calculating the squared and cubed values of t.

I was then going to use linear interpolation in order to generate an array of points where the values of x are linearly seperated (e.g x = 0.01, 0.02... 0.99) and then use this array to define my animation progress for the time elapsed.

Is this the best way of going about this problem? It seems like a fairly intensive process for quite a simple task, even though it could all be pre-calculated at launch.

I saw on this question the answerer recommended simply eliminating x and define the function as y against t. However, this will give pretty inaccurate results, as a linear Bezier curve from 0.0 to 1.0 will not animate linearly.

Is there a more efficient approach to this?

Does anyone know how Apple achieves this in CAMediaTimingFunction or is there a library out there that achieves the same result?

Or is there a simpler alternative to using a cubic Bezier function that will give a smooth animation curve on both the in and out stages of the animation?

Community
  • 1
  • 1
Hamish
  • 78,605
  • 19
  • 187
  • 280
  • It sounds like your question is basically, “How do I sample points equally spaced along a Bézier cubic curve?” Check out http://stackoverflow.com/a/26131682/77567 and http://stackoverflow.com/a/17227585/77567 and http://stackoverflow.com/a/13667092/77567 – rob mayoff Dec 24 '15 at 17:42
  • Well those questions are focussed on drawing Bezier curves using `CGPath`. I'm sorry if I didn't quite make it explicit in the question, but I'd like to stay away from UIKit as much as possible as I'm doing this in OpenGL and would like it to be as multi-platform as possible. Also none of them actually help with getting a linear set of values of a Bezier curve (you cannot read a set of points from a `CGPath`, you can only render along it). – Hamish Dec 24 '15 at 18:02
  • 1
    You should take `ios` out of your tags if you want a multiplatform answer. Incidentally, those answers **do** read a linear set of values along a `CGPath`, using the `CGPathCreateCopyByDashingPath` copy function. – rob mayoff Dec 24 '15 at 18:05
  • oh I see using `CGPathApply` along with `CGPathCreateCopyByDashingPath ` .. seems quite cumbersome for what I want to do anyway... so thanks but no thanks! – Hamish Dec 24 '15 at 18:33
  • also, thinking about it some more, you haven't understood my question at all. I'm not asking how you sample points **equally spaced** along a Bezier curve, I'm asking how you sample points with an **equally spaced x-value** along a Bezier curve. Your `CGPath` method would simply result in the same values generated by the `bezierFunc1D` function in the question. – Hamish Dec 24 '15 at 18:41
  • 1
    My prior (now deleted) comment about how Apple does it was incorrect. [Here's Apple's code to sample a timing function, from WebKit.](https://github.com/WebKit/webkit/blob/67985c34ffc405f69995e8a35f9c38618625c403/Source/WebCore/platform/graphics/UnitBezier.h) [Example call here.](https://github.com/WebKit/webkit/blob/67985c34ffc405f69995e8a35f9c38618625c403/Source/WebCore/platform/graphics/texmap/TextureMapperAnimation.cpp#L126) – rob mayoff Dec 24 '15 at 19:56
  • Is your animation curve fixed (i.e. do you already know what the function looks like), or is it dynamic? Because in the first case, we can always convert the curve to a non-parametric `y = f(x)` form. This is different from "forgetting the x component" (which is rather wrong) and instead gives us a function that literally does what you need it to do. In this case, the *best* solution is to not bother using a parametric curve when you need linear traversal. Don't use a Bezier curve, but a [sigmoid](https://en.wikipedia.org/wiki/Sigmoid_function) or the like – Mike 'Pomax' Kamermans Dec 24 '15 at 20:07
  • No, sadly it's dynamic. Rob's answer looks like it's got me covered with a combination of Newton-Raphson & Bisection. – Hamish Dec 24 '15 at 20:10
  • you shouldn't need bisection fallback given the intervals you're working with - the kind of curves that are allowed in CSS are too well-behaved for N-R iteration to fail (except for degenerate and near-degenerate curves, which you'd generally have detection for so your code straight up uses a line, instead of a bezier). It's necessary for a correct CSS implementation, but shouldn't be necessary for your use case. – Mike 'Pomax' Kamermans Dec 24 '15 at 20:18
  • Well no, but I'll probably leave it in to future proof my code, in case I ever have to work with more complex cubics in future – Hamish Dec 24 '15 at 20:25

1 Answers1

2

This is how Apple samples the CSS equivalent of CAMediaTimingFunction in WebKit. The function of interest is double solve(double x, double epsilon), which returns the y for x ± epsilon.

/*
 * Copyright (C) 2008 Apple Inc. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#ifndef UnitBezier_h
#define UnitBezier_h

#include <math.h>

namespace WebCore {

    struct UnitBezier {
        UnitBezier(double p1x, double p1y, double p2x, double p2y)
        {
            // Calculate the polynomial coefficients, implicit first and last control points are (0,0) and (1,1).
            cx = 3.0 * p1x;
            bx = 3.0 * (p2x - p1x) - cx;
            ax = 1.0 - cx -bx;

            cy = 3.0 * p1y;
            by = 3.0 * (p2y - p1y) - cy;
            ay = 1.0 - cy - by;
        }

        double sampleCurveX(double t)
        {
            // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule.
            return ((ax * t + bx) * t + cx) * t;
        }

        double sampleCurveY(double t)
        {
            return ((ay * t + by) * t + cy) * t;
        }

        double sampleCurveDerivativeX(double t)
        {
            return (3.0 * ax * t + 2.0 * bx) * t + cx;
        }

        // Given an x value, find a parametric value it came from.
        double solveCurveX(double x, double epsilon)
        {
            double t0;
            double t1;
            double t2;
            double x2;
            double d2;
            int i;

            // First try a few iterations of Newton's method -- normally very fast.
            for (t2 = x, i = 0; i < 8; i++) {
                x2 = sampleCurveX(t2) - x;
                if (fabs (x2) < epsilon)
                    return t2;
                d2 = sampleCurveDerivativeX(t2);
                if (fabs(d2) < 1e-6)
                    break;
                t2 = t2 - x2 / d2;
            }

            // Fall back to the bisection method for reliability.
            t0 = 0.0;
            t1 = 1.0;
            t2 = x;

            if (t2 < t0)
                return t0;
            if (t2 > t1)
                return t1;

            while (t0 < t1) {
                x2 = sampleCurveX(t2);
                if (fabs(x2 - x) < epsilon)
                    return t2;
                if (x > x2)
                    t0 = t2;
                else
                    t1 = t2;
                t2 = (t1 - t0) * .5 + t0;
            }

            // Failure.
            return t2;
        }

        double solve(double x, double epsilon)
        {
            return sampleCurveY(solveCurveX(x, epsilon));
        }

    private:
        double ax;
        double bx;
        double cx;

        double ay;
        double by;
        double cy;
    };
}
#endif

Source: https://github.com/WebKit/webkit/blob/67985c34ffc405f69995e8a35f9c38618625c403/Source/WebCore/platform/graphics/UnitBezier.h

rob mayoff
  • 375,296
  • 67
  • 796
  • 848