Source: level0/animate.js

/**
 * `animate` functions manipulate a state object according to the inputs t,
 * state, begin, end. As an animation steps it will call its animate function
 * with a progressing `t` value commonly being the number of seconds divided by
 * the duration of the animation.
 *
 * t is a time interval unit ranging from values 0 to 1. 
 *
 * @module animate
 * @example
 * // Return the value for this function at point t (0 to 1) with the input
 * // state, begin, end. If state is an object the value can be set directly
 * // to a member of state. The update function will ensure that state, begin,
 * // and end, have the same shape and are defined.
 * function f(t, state, begin, end) {}
 * // Return the value for this function optionally considering function b to
 * // be the target destination at t = 1. Think of this function as from `a`
 * // to `b`.
 * f.a = function(b, t, state, begin, end) {}
 * // Return true if state has reached what this function considers to be the
 * // end. Some functions may think that is t >= 1. Some functions may think
 * // that is state === end.
 * f.eq = function(t, state, begin, end) {}
 */

const compileRegistry = require('./compile-registry');
const inlined = compileRegistry.inlined;

/**
 * Create an animate function that calls the passed function argument with t,
 * state, begin, and end return its result.
 *
 * @function value
 */
let value = inlined(function value(fn) {
  const f = function(t, state, begin, end, data) {
    return fn(t, state, begin, end, data);
  };
  return f;
});

/**
 * Create a function with the output of another animate call and a a-to-b
 * function to replace the a-to-b from the other animate call.
 *
 * @function a
 */
let toB = inlined(function toB(fn, toBFn) {
  const f = function(t, state, begin, end, data) {
    return fn(t, state, begin, end, data);
  };
  f.toB = function(b, t, state, begin, end, data) {
    return toBFn(b, t, state, begin, end, data);
  };
  return f;
});

/**
 * Create a function with the output of another animate call and an eq
 * when-animate-is-complete function to replace the eq from the other animate
 * call.
 *
 * @function done
 */
let done = inlined(function done(fn, doneFn) {
  const f = function(t, state, begin, end, data) {
    return fn(t, state, begin, end, data);
  };
  f.toB = function(b, t, state, begin, end, data) {
    return fn.toB ? fn.toB(b, t, state, begin, end, data) : fn(t, state, begin, end, data);
  };
  f.done = function(t, state, begin, end, data) {
    return doneFn(t, state, begin, end, data);
  };
  return f;
});

/**
 * Create an animate function that calls the passed function and returns the
 * result. It's a-to-b method calls the passed function and the b and
 * interpolates between them depending on `t`.
 *
 * @function lerp
 */
let lerp = inlined(function lerp(fn) {
  return done(
    toB(
      fn,
      function(b, t, state, begin, end, data) {
        const _b = fn(t, state, begin, end, data);
        const e = b(t, state, begin, end, data);
        return (e - _b) * Math.min(1, t) + _b;
      }
    ),
    function(t) {return t >= 1;}
  );
});

/**
 * Create an animate function that calls each function in the given array with
 * the same t, state, begin, and end values. This is useful to perform
 * different behaviours on the same object in the state's shape.
 *
 * @function union
 */
let union = inlined(function union(set) {
  const f = function(t, state, begin, end, data) {
    for (const value of Object.values(set)) {
      value(t, state, begin, end, data);
    }
    return state;
  };
  f.set = set;
  f.toB = function(b, t, state, begin, end, data) {
    let i = 0;
    for (const value of Object.values(set)) {
      value.toB(b.set[i], t, state, begin, end, data);
      i = i + 1;
    }
    return state;
  };
  f.done = function(t, state, begin, end, data) {
    let result = true;
    let i = 0;
    for (const value of Object.values(set)) {
      if (result && !value.done(t, state, begin, end, data)) {
        result = false;
      }
    }
    return result;
  };
  return f;
});

let unary = inlined(function unary(op, fn) {
  return value(function(t, state, begin, end, data) {
    return op(fn(t, state, begin, end, data));
  });
});

let binary = inlined(function binary(op, fn1, fn2) {
  return value(function(t, state, begin, end, data) {
    return op(fn1(t, state, begin, end, data), fn2(t, state, begin, end, data));
  });
});

/**
 * Create a function that returns the absolute value of the returned result of
 * the passed function.
 *
 * @function abs
 */
let abs = inlined(function abs(fn) {
  return unary(function(v) {return Math.abs(v);}, fn);
});

/**
 * Create a function that returns the sumed value of the returned results of
 * the two passed functions.
 *
 * @function add
 */
let add = inlined(function add(fn1, fn2) {
  return binary(function(a, b) {return a + b;}, fn1, fn2);
});

/**
 * Create a function that returns the difference of the returned results of
 * the two passed functions.
 *
 * @function sub
 */
let sub = inlined(function sub(fn1, fn2) {
  return binary(function(a, b) {return a - b;}, fn1, fn2);
});

/**
 * Create a function that returns the multiplied value of the returned results
 * of the two passed functions.
 *
 * @function mul
 */
let mul = inlined(function mul(fn1, fn2) {
  return binary(function(a, b) {return a * b;}, fn1, fn2);
});

/**
 * Create a function that returns the divided value of the returned results of
 * the two passed functions.
 *
 * @function div
 */
let div = inlined(function div(fn1, fn2) {
  return binary(function(a, b) {return a / b;}, fn1, fn2);
});

/**
 * Create a function that returns the remainder of the divided of the returned
 * results of the two passed functions.
 *
 * @function mod
 */
let mod = inlined(function mod(fn1, fn2) {
  return binary(function(a, b) {return a % b;}, fn1, fn2);
});

/**
 * Create a function that returns the minimum value of the returned results of
 * the two passed functions.
 *
 * @function min
 */
let min = inlined(function min(fn1, fn2) {
  return binary(function(a, b) {return Math.min(a, b);}, fn1, fn2);
});

/**
 * Create a function that returns the maximum value of the returned results of
 * the two passed functions.
 *
 * @function max
 */
let max = inlined(function max(fn1, fn2) {
  return binary(function(a, b) {return Math.max(a, b);}, fn1, fn2);
});

/**
 * Create a function that returns whether the returned results of
 * the two passed functions are equivalent.
 *
 * @function eq
 */
let eq = inlined(function eq(fn1, fn2) {
  return binary(function(a, b) {return a === b;}, fn1, fn2);
});

/**
 * Create a function that returns whether the returned results of
 * the two passed functions are different.
 *
 * @function ne
 */
let ne = inlined(function ne(fn1, fn2) {
  return binary(function(a, b) {return a !== b;}, fn1, fn2);
});

/**
 * Create a function that returns whether the returned result of
 * the first passed functions is less than the second passed function's result.
 *
 * @function lt
 */
let lt = inlined(function lt(fn1, fn2) {
  return binary(function(a, b) {return a < b;}, fn1, fn2);
});

/**
 * Create a function that returns whether the returned result of the first
 * passed functions is less than or equal to the second passed function's
 * result.
 *
 * @function lte
 */
let lte = inlined(function lte(fn1, fn2) {
  return binary(function(a, b) {return a <= b;}, fn1, fn2);
});

/**
 * Create a function that returns whether the returned result of
 * the first passed functions is greater than the second passed function's
 * result.
 *
 * @function gt
 */
let gt = inlined(function gt(fn1, fn2) {
  return binary(function(a, b) {return a > b;}, fn1, fn2);
});

/**
 * Create a function that returns whether the returned result of the first
 * passed functions is greater than or equal to the second passed function's
 * result.
 *
 * @function gte
 */
let gte = inlined(function gte(fn1, fn2) {
  return binary(function(a, b) {return a >= b;}, fn1, fn2);
});

/**
 * Create a function that returns the given constant value.
 *
 * @function constant
 */
let constant = inlined(function constant(c) {
  return value(function() {return c;});
});

/**
 * Create a function that returns the passed t value.
 *
 * @function t
 */
let t = inlined(function t() {
  return value(function(t) {return t;});
});

/**
 * Create a function that returns the passed state value.
 *
 * @function state
 */
let state = inlined(function state() {
  return value(function(t, state) {return state;});
});

/**
 * Create a function that passes a value between the begin and end value based
 * on the given position (pos) value. A 0 position value is the begin value. A
 * 1 position value is the end value. Any value between 0 and 1 is
 * proprotionally positioned on the line between begin and end.
 *
 * @function at
 */
let at = inlined(function at(pos) {
  return lerp(function(t, state, begin, end) {
    return (end - begin) * pos + begin;
  });
});

/**
 * Create a function that returns the begin value.
 *
 * @function begin
 */
let begin = inlined(function begin() {
  return at(0);
});

/**
 * Create a function that returns the end value.
 *
 * @function end
 */
let end = inlined(function end() {
  return at(1);
});

/**
 * Create a function that returns the value portionally based on t between the
 * first and second function in the passed array.
 *
 * @function to
 */
let to = inlined(function to(a, b) {
  return toB(function(t, state, begin, end, data) {
    return a.toB(b, t, state, begin, end, data);
  }, function(_b, t, state, begin, end, data) {
    return a.toB(_b, t, state, begin, end, data);
  });
});

/**
 * Create a function that calls the passed second argument with the key member
 * of the state, begin, and end values.
 *
 * @function get
 */
let get = inlined(function get(key, fn) {
  return value(function(t, state, begin, end, data) {
    return fn(t, state[key], begin[key], end[key], data);
  });
});

/**
 * Create a function that sets the key's member of the state value with the
 * result of the called function.
 *
 * @function set
 */
let set = inlined(function set(key, fn) {
  return value(function(t, state, begin, end, data) {
    state[key] = fn(t, state, begin, end, data);
    return state;
  });
});

/**
 * Create a function that iterates the given object's keys and values, setting
 * the state's key member by the called value function given the key member of
 * state, begin, and end.
 *
 * @function object
 */
let object = inlined(function object(obj) {
  const f = function(t, state, begin, end, data) {
    for (const [key, value] of Object.entries(obj)) {
      state[key] = value(t, state[key], begin[key], end[key], data);
    }
    return state;
  };
  f.obj = obj;
  f.toB = function(b, t, state, begin, end, data) {
    for (const [key, value] of Object.entries(obj)) {
      state[key] = value.toB(b.obj[key], t, state[key], begin[key], end[key], data);
    }
    return state;
  };
  return f;
});

/**
 * Create a function that calls the given function with every indexed member of
 * state, begin, and end.
 *
 * @function array
 */
let array = inlined(function array(fn) {
  return toB(function(t, state, begin, end, data) {
    for (let i = 0; i < state.length; i++) {
      state[i] = fn(t, state[i], begin[i], end[i], data);
    }
    return state;
  }, fn.toB || fn);
});

/**
 * Create a function that transforms t by one function and passing that into
 * the first function along with state, begin, and end.
 *
 * @function easing
 */
let easing = inlined(function easing(fn, tfn) {
  return done(toB(function(t, state, begin, end, data) {
    return fn(tfn(t, state, begin, end, data), state, begin, end, data);
  }, fn.toB || fn), function(t, state, begin, end, data) {
    return tfn(t, state, begin, end, data) >= 1;
  });
});

/**
 * Create a function that calls the passed function with t transformed by a
 * cubic ease-in function.
 *
 * @function easeIn
 */
let easeIn = inlined(function easeIn(fn) {
  return easing(fn, function(t) {return t * t * t;});
});

/**
 * Create a function that calls the passed function with t transformed by a
 * cubic ease-out function.
 *
 * @function easeOut
 */
let easeOut = inlined(function easeOut(fn) {
  return easing(fn, function(t) {return (t - 1) * (t - 1) * (t - 1) + 1;});
});

/**
 * Create a function that calls the passed function with t transformed by a
 * cubic ease-in-out function.
 *
 * @function easeInOut
 */
let easeInOut = inlined(function easeInOut(fn) {
  return easing(fn, function(t) {
    let out;
    if (t < 0.5) {
      out = 4 * t * t * t;
    }
    else {
      out = (t - 1) * (t * 2 - 2) * (t * 2 - 2) + 1;
    }
    return out;
  });
});

// const bezierC = ast.context(({
//   func, mul, l, r, w,
// }) => (
//   func(['x1', 'x2'], [
//     mul(l(3), r('x1')),
//   ])
// ));
//
// const bezierB = ast.context(({
//   func, sub, mul, l, r, w, call,
// }) => (
//   func(['x1', 'x2'], [
//     // sub(mul(l(3), r('x2')), mul(l(6), r('x1'))),
//     w('c', call(bezierC, [r('x1'), r('x2')])),
//     sub(mul(l(3), sub(r('x2'), r('x1'))), r('c')),
//   ])
// ));
//
// const bezierA = ast.context(({
//   func, sub, l, mul, r, w, call,
// }) => (
//   func(['x1', 'x2'], [
//     // sub(sub(l(1), mul(l(3), r('x2'))), mul(l(3), r('x1'))),
//     w('c', call(bezierC, [r('x1'), r('x2')])),
//     w('b', call(bezierB, [r('x1'), r('x2')])),
//     sub(sub(l(1), r('c')), r('b')),
//   ])
// ));
//
// const bezierBez = ast.context(({
//   func, mul, r, add, call, w, l,
// }) => (
//   // t * (c(x1, x2) + t * (b(x1, x2) + t * a(x1, x2)));
//   func(['t', 'x1', 'x2'], [
//     w('c', call(bezierC, [r('x1'), r('x2')])),
//     w('b', call(bezierB, [r('x1'), r('x2')])),
//     w('a', call(bezierA, [r('x1'), r('x2')])),
//     mul(
//       r('t'),
//       add(
//         r('c'),
//         mul(
//           r('t'),
//           add(
//             r('b'),
//             mul(
//               r('t'),
//               r('a')
//             )
//           )
//         )
//       )
//     ),
//   ])
// ));
//
// const bezierDeriv = ast.context(({
//   func, add, call, r, mul, l, w,
// }) => (
//   func(['t', 'x1', 'x2'], [
//     // c(x1, x2) + t * (2 * b(x1, x2) + 3 * a(x1, x2) * t);
//     w('c', call(bezierC, [r('x1'), r('x2')])),
//     w('b', call(bezierB, [r('x1'), r('x2')])),
//     w('a', call(bezierA, [r('x1'), r('x2')])),
//     add(
//       r('c'),
//       mul(
//         r('t'),
//         add(
//           mul(l(2), r('b')),
//           mul(
//             mul(l(3), r('a')),
//             r('t')
//           )
//         )
//       )
//     )
//   ])
// ));
//
// const bezierSearch = ast.context(({
//   func, w, r, l, call, loop, and, lt, gte, abs, sub, div, add, branch,
// }) => (
//   func(['t', 'x1', 'x2'], [
//     w('x', r('t')),
//     w('i', l(0)),
//     w('z', sub(call(bezierBez, [r('x'), r('x1'), r('x2')]), r('t'))),
//     loop(and(lt(r('i'), l(14)), gte(abs(r('z')), l(1e-3))), [
//       w('x', sub(r('x'), div(r('z'), call(bezierDeriv, [r('x'), r('x1'), r('x2')])))),
//       w('z', sub(call(bezierBez, [r('x'), r('x1'), r('x2')]), r('t'))),
//       w('i', add(r('i'), l(1))),
//     ]),
//     branch(r('z'), []),
//     branch(r('i'), []),
//     r('x'),
//   ])
// ));
//
// /**
//  * Create a function that calls the passed function with t transformed by a
//  * cubic bezier function.
//  *
//  * @function bezier
//  */
// const bezierArgs = [['fn', 'ax', 'ay', 'bx', 'by'], r('fn'), r('ax'), r('ay'), r('bx'), r('by')];
// const bezier = ast.context(({
//   func, call, r, l, w, methods,
// }) => (fn, ax, ay, bx, by) => (
//   easing(fn, func(['t'], [
//     call(bezierBez, [call(bezierSearch, [r('t'), l(ax), l(bx)]), l(ay), l(by)]),
//   ]))
// ));
// bezier.args = bezierArgs;

/**
 * Create a function that calls the passed function with t transformed by
 * dividing t by a given constant duration.
 *
 * @function duration
 */
let duration = inlined(function duration(fn, length) {
  return easing(fn, function(t) {return t / length});
});

/**
 * Creates a function that calls the passed function with t transformed by
 * using its remainder divided by a constant value.
 *
 * @function loop
 */
let loop = inlined(function loop(fn, loop) {
  return easing(fn, function(t) {return t / loop % 1;});
});

/**
 * Creates a function that calls the passed function with until the until
 * function returns a value greater or than 1.
 *
 * @function repeat
 */
let repeat = inlined(function repeat(fn, until) {
  return done(fn, until);
});

// const keyframesSum = _frames => {
//   if (Array.isArray(_frames)) {
//
//   }
//   else {
//     let sum = 0;
//     return frames => {
//       if (sum === 0) {
//
//       }
//       return sum;
//     };
//   }
// };
let keyframes = inlined(function keyframes(frames) {
  const f = function(_t, state, begin, end, data) {
    const sum = (function(frames) {
      let s = 0;
      for (const value of frames) {
        s = s + value.t();
      }
      return s;
    })(frames);

    let out = state;
    let t = Math.max(Math.min(_t * sum, sum), 0);
    let i = 0;
    for (const value of frames) {
      if (i > 0 && t <= 0) {
        out = frames[i - 1].toB(value, (t + frames[i - 1].t()) / frames[i - 1].t(), state, begin, end, data);
        t = t + Infinity;
      }
      else {
        t = t - value.t();
      }
      i = i + 1;
    }
    return out;
  };

  f.toB = function(b, _t, state, begin, end, data) {
    const sum = (function(frames) {
      let s = 0;
      for (const value of frames) {
        s = s + value.t();
      }
      return s;
    })(frames);

    let out = state;
    let t = Math.max(Math.min(_t * sum, sum), 0);
    let i;
    // TODO: Fix plugin to be able to safely use non constant declarations.
    i = 0;
    for (const value of frames) {
      t = t - value.t();
      if (t <= 0) {
        out = value.toB(
          frames[i + 1] || b,
          (t + value.t()) / value.t(),
          state,
          begin,
          end,
          data
        );
        t = t + Infinity;
      }
      i = i + 1;
    }
    return out;
  };

  return f;
});

let frame = inlined(function frame(timer, fn) {
  const f = function(t, state, begin, end, data) {
    return fn(timer(t), state, begin, end, data);
  };
  f.toB = function(b, t, state, begin, end, data) {
    let result;
    if (fn.toB) {
      result = fn.toB(b.fn || b, t, state, begin, end, data);
    }
    else {
      const _b = fn(t, state, begin, end, data);
      const e = b(t, state, begin, end, data);
      result = (e - _b) * Math.min(1, t) + _b;
    }
    return result;
  };
  f.t = function() {return timer.t();};
  f.fn = fn;
  return f;
});

let timer = inlined(function timer(unit) {
  const fTimer = function(t) {return t / unit;};
  fTimer.t = function() {return unit;};
  fTimer.toB = null;
  fTimer.done = null;
  return fTimer;
});

let seconds = inlined(function seconds(seconds) {
  return timer(seconds);
});

let ms = inlined(function ms(ms) {
  return timer(ms / 1000);
});

let percent = inlined(function percent(percent) {
  return timer(percent / 100);
});

module.exports = compileRegistry({
  value,
  lerp,
  union,
  toB,
  done,
  unary,
  abs,
  binary,
  add, sub, mul, div, mod, min, max, eq, ne, lt, lte, gt, gte,
  constant,
  t,
  state,
  at,
  begin,
  end,
  to,
  get,
  set,
  object,
  array,
  easing,
  easeIn,
  easeOut,
  easeInOut,
  // bezier,
  duration,
  keyframes,
  frame,
  timer,
  seconds,
  ms,
  percent,
  until: repeat,
  loop,
});