<commit_before>// @flow
/**
 * AnimationMachine
 *
 * @module internal/machines/animation-machine
 */

import BezierEasing from 'bezier-easing'
import Debug from 'debug'

import type { Time, Controls, Animation, AnimationStage } from '../flow-types'

import Constants from '../constants'
import { constructStyles } from '../fashionistas/timed-fashionista'
import { InfiniteTimeMachine, FiniteTimeMachine } from './time-machine'
import { SpringMachine } from './spring-machine'

const debug = Debug('animatronics:animation');

const DEFAULT_EASING_FN = BezierEasing(0.4, 0.0, 0.2, 1);

const isUsingTime = (animation: Object): boolean =>
  animation.duration != null;

const isUsingSpring = (animation: Object): boolean =>
  animation.stiffness != null && animation.damping != null;

export const findLongestDelay = (stage: AnimationStage): number =>
  Object.keys(stage)
    .map(key => stage[key])
    .map(s => s.delay || 0)
    .reduce(
      (longest, delay) => (delay > longest) ? delay : longest,
      0
    );

export const reverseStages = (stages: AnimationStage[]): AnimationStage[] =>
  stages
    .map(stage => Object.keys(stage)
      .reverse()
      .reduce((result, componentName) => {
        const longestDelay = findLongestDelay(stage);
        const { start, end, delay, ...rest } = stage[componentName];
        result[componentName] = {
          delay: longestDelay - (delay || 0),
          start: end,
          end: start,
          ...rest,
        };
        return result;
      }, {})
    )
    .reverse();

export const calculateEasingProgress = (
  easingFn: Function,
  duration: number,
  elapsedTime: number,
): number =>
  easingFn(elapsedTime / (duration === 0 ? elapsedTime : duration));

const runTimedAnimation = (
  timeMachine: Time,
  animation: Object,
  onFrame: Function,
  onComponentDone: Function,
) => {
  const {
    start: startStyles,
    end: endStyles,
    duration,
    easingFn = DEFAULT_EASING_FN,
  } = animation;

  const finiteMachine: Time = FiniteTimeMachine(timeMachine, duration);

  finiteMachine
    .do(elapsedTime => {
      const progress = calculateEasingProgress(easingFn, duration, elapsedTime);
      const currentStyles = constructStyles(startStyles, endStyles, progress);
      onFrame(currentStyles);
    })
    .run(() => {
      onFrame(endStyles);
      onComponentDone();
    });
}

const runSpringAnimation = (
  timeMachine: Time,
  animation: Object,
  onFrame: Function,
  onComponentDone: Function,
) => {
  const {
    start: startStyles,
    end: endStyles,
    stiffness,
    damping,
  } = animation;

  const springMachine = SpringMachine(startStyles, endStyles, stiffness, damping);
  const _onNext = (updatedStyles) => {
    onFrame(updatedStyles);
  }
  const _onComplete = (endStyles) => {
    onFrame(endStyles);
    timeMachine.stop();
    onComponentDone();
  }

  timeMachine
    .do(() => {
      springMachine.next(_onNext, _onComplete);
    })
    .run();
}

export default (
  createAnimationStages: Function,
  requestAnimationFrame: Function,
  cancelAnimationFrame: Function,
): Animation => {

  const _state: Object = {
    timeMachines: null,
    stages: null,
    createAnimationStages,
  };

  const _runStage = (
    stage: AnimationStage,
    onComponentFrame: Function,
    onStageComplete: Function,
  ): void => {
    debug('running animation stage %O', stage);

    _state.timeMachines = {};

    const componentNames = Object.keys(stage);
    let numComponentsDone = 0;

    const onComponentDone = () => {
      numComponentsDone++;
      if (numComponentsDone === componentNames.length) {
        onStageComplete();
      }
    };

    componentNames.forEach(componentName => {
      const animation: Object = stage[componentName];
      _state.timeMachines[componentName] = InfiniteTimeMachine(
        requestAnimationFrame,
        cancelAnimationFrame,
      );

      const onFrame: Function = (updatedStyles) => {
        onComponentFrame(componentName, updatedStyles);
      }

      if (isUsingTime(animation)) {
        runTimedAnimation(
          _state.timeMachines[componentName],
          animation,
          onFrame,
          onComponentDone
        );
      } else if (isUsingSpring(animation)) {
        runSpringAnimation(
          _state.timeMachines[componentName],
          animation,
          onFrame,
          onComponentDone
        );
      } else {
        // TODO: Error
      }
    });
  }

  const _onStageComplete = (
    nextStageNum,
    animationStages,
    onComponentFrame,<commit_msg>:sparkles: [animation-machine] Allow callers to specify delays (late commit)<commit_after>// @flow
/**
 * AnimationMachine
 *
 * @module internal/machines/animation-machine
 */

import BezierEasing from 'bezier-easing'
import Debug from 'debug'

import type { Time, Controls, Animation, AnimationStage } from '../flow-types'

import Constants from '../constants'
import { constructStyles } from '../fashionistas/timed-fashionista'
import { InfiniteTimeMachine, FiniteTimeMachine } from './time-machine'
import { SpringMachine } from './spring-machine'

const debug = Debug('animatronics:animation');

const DEFAULT_EASING_FN = BezierEasing(0.4, 0.0, 0.2, 1);

const isUsingTime = (animation: Object): boolean =>
  animation.duration != null;

const isUsingSpring = (animation: Object): boolean =>
  animation.stiffness != null && animation.damping != null;

export const findLongestDelay = (stage: AnimationStage): number =>
  Object.keys(stage)
    .map(key => stage[key])
    .map(s => s.delay || 0)
    .reduce(
      (longest, delay) => (delay > longest) ? delay : longest,
      0
    );

export const reverseStages = (stages: AnimationStage[]): AnimationStage[] =>
  stages
    .map(stage => Object.keys(stage)
      .reverse()
      .reduce((result, componentName) => {
        const longestDelay = findLongestDelay(stage);
        const { start, end, delay, ...rest } = stage[componentName];
        result[componentName] = {
          delay: longestDelay - (delay || 0),
          start: end,
          end: start,
          ...rest,
        };
        return result;
      }, {})
    )
    .reverse();

export const calculateEasingProgress = (
  easingFn: Function,
  duration: number,
  elapsedTime: number,
): number =>
  easingFn(elapsedTime / (duration === 0 ? elapsedTime : duration));

const runTimedAnimation = (
  timeMachine: Time,
  animation: Object,
  onFrame: Function,
  onComponentDone: Function,
) => {
  const {
    start: startStyles,
    end: endStyles,
    duration,
    easingFn = DEFAULT_EASING_FN,
  } = animation;

  const finiteMachine: Time = FiniteTimeMachine(timeMachine, duration);

  finiteMachine
    .do(elapsedTime => {
      const progress = calculateEasingProgress(easingFn, duration, elapsedTime);
      const currentStyles = constructStyles(startStyles, endStyles, progress);
      onFrame(currentStyles);
    })
    .run(() => {
      onFrame(endStyles);
      onComponentDone();
    });
}

const runSpringAnimation = (
  timeMachine: Time,
  animation: Object,
  onFrame: Function,
  onComponentDone: Function,
) => {
  const {
    start: startStyles,
    end: endStyles,
    stiffness,
    damping,
  } = animation;

  const springMachine = SpringMachine(startStyles, endStyles, stiffness, damping);
  const _onNext = (updatedStyles) => {
    onFrame(updatedStyles);
  }
  const _onComplete = (endStyles) => {
    onFrame(endStyles);
    timeMachine.stop();
    onComponentDone();
  }

  timeMachine
    .do(() => {
      springMachine.next(_onNext, _onComplete);
    })
    .run();
}

export default (
  createAnimationStages: Function,
  requestAnimationFrame: Function,
  cancelAnimationFrame: Function,
): Animation => {

  const _state: Object = {
    timeMachines: null,
    stages: null,
    createAnimationStages,
  };

  const _runStage = (
    stage: AnimationStage,
    onComponentFrame: Function,
    onStageComplete: Function,
  ): void => {
    debug('running animation stage %O', stage);

    _state.timeMachines = {};

    const componentNames = Object.keys(stage);
    let numComponentsDone = 0;

    const onComponentDone = () => {
      numComponentsDone++;
      if (numComponentsDone === componentNames.length) {
        onStageComplete();
      }
    };

    const runComponentStage = componentName => {
      const animation: Object = stage[componentName];

      _state.timeMachines[componentName] = InfiniteTimeMachine(
        requestAnimationFrame,
        cancelAnimationFrame,
      );

      const onFrame: Function = (updatedStyles) => {
        onComponentFrame(componentName, updatedStyles);
      }

      if (isUsingTime(animation)) {
        runTimedAnimation(
          _state.timeMachines[componentName],
          animation,
          onFrame,
          onComponentDone
        );
      } else if (isUsingSpring(animation)) {
        runSpringAnimation(
          _state.timeMachines[componentName],
          animation,
          onFrame,
          onComponentDone
        );
      } else {
        // TODO: Error
      }

    }

    componentNames.forEach(componentName => {
      const animation: Object = stage[componentName];
      // Intentionally ignoring 0 (in addition to null and undefined) even though
      // setTimeout(fn, 0) would affect the execution timing.
      if (!animation.delay) {
        runComponentStage(componentName);
      } else {
        setTimeout(() => runComponentStage(componentName), animation.delay);
      }
    });
  }

  const _onStageComplete = (
    nextStageNum,
    animationStages,
    onComponentFrame,
{'fraction_non_alphanumeric': 0.11883150756390193, 'fraction_numerical': 0.002712571726656234, 'mean_word_length': 2.9611570247933883, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 51, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '358413', 'n_tokens_mistral': 2812, 'n_tokens_neox': 2769, 'n_words': 627}