chromium/chrome/browser/resources/compose/animations/app_animator.ts

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {isMac} from '//resources/js/platform.js';

import {Animator, EMPHASIZED_DECELERATE, STANDARD_EASING} from './animator.js';

export class ComposeAppAnimator extends Animator {
  transitionOutSubmitFooter(bodyHeight: number, footerHeight: number):
      Animation[] {
    return [
      /* Freeze dialog heights while footer fade out finishes. */
      this.maintainStyles(
          '#bodyAndFooter', {
            gridTemplateAreas: '"body" "footer"',
            gridTemplateRows: `${bodyHeight}px ${footerHeight}px`,
          },
          {duration: 50}),
      this.fadeOutAndHide('#submitFooter', 'flex', {duration: 50}),
    ].flat();
  }

  transitionToFirstRun(): Animation[] {
    const firstRunScreenText = '#firstRunHeading h1, #firstRunContainer';
    const firstRunScreenButtons = '#firstRunCloseButton, #firstRunFooter';
    return [
      this.scaleIn('#firstRunIconContainer', {duration: 250}),
      this.slideIn(firstRunScreenText, -8, {duration: 250}),
      this.fadeIn(firstRunScreenText, {delay: 50, duration: 100}),
      this.fadeIn(firstRunScreenButtons, {delay: 100, duration: 100}),
    ].flat();
  }

  transitionToInput(): Animation[] {
    // Need to queue the fade out animation first to prevent the FRE
    // dialog from becoming immediately hidden. Otherwise, height calculations
    // below will have values of 0 since the dialog is hidden.
    const firstRunFadeOut =
        this.fadeOutAndHide('#firstRunDialog', 'flex', {duration: 100});

    const contentMoveDistance = 48;
    const firstRunContainerHeight =
        this.getElement('#firstRunContainer').offsetHeight;
    const firstRunContainerHeightAnimation = this.animate(
        '#firstRunContainer',
        [
          {height: `${firstRunContainerHeight}px`},
          {height: `${firstRunContainerHeight - contentMoveDistance}px`},
        ],
        {duration: 200, easing: STANDARD_EASING});

    const inputScreenText = '#heading h1, #body, #submitFooter .footer-text';
    return [
      firstRunFadeOut,
      firstRunContainerHeightAnimation,
      this.slideOut(
          '#firstRunHeading h1, #firstRunContainer', -8, {duration: 200}),
      this.fadeOut('#firstRunFooter', {duration: 100}),
      this.slideIn(inputScreenText, 8, {duration: 200}),
      this.slideIn('#submitButton', contentMoveDistance, {duration: 200}),
      this.fadeIn(inputScreenText, {delay: 100, duration: 100}),
      this.fadeIn('#submitButton', {delay: 100, duration: 100}),
    ].flat();
  }

  transitionInDialog() {
    return [
      this.slideIn('.dialog:not([hidden])', -8, {duration: 200}),
      this.fadeIn('.dialog:not([hidden])', {duration: 200}),
    ];
  }

  transitionInLoading(): Animation[] {
    return this.fadeIn('#loading', {delay: 100, duration: 100});
  }

  transitionFromEditingToLoading(bodyHeight: number): Animation[] {
    return [
      // Shrink #bodyAndFooter from the current expanded height of the edit UI
      // to the loading state.
      this.animate(
          '#bodyAndFooter',
          [
            {height: `${bodyHeight}px`},
            {height: `var(--compose-loading-body-and-footer-height)`},
          ],
          {duration: 250, easing: STANDARD_EASING}, !isMac),

      // Fade out the edit form.
      this.fadeOutAndHide('#editContainer', 'flex', {duration: 250}),

      // The footer for the edit form fades out faster.
      this.fadeOut('#editContainer .footer', {duration: 50}),
      this.maintainStyles(
          '#editContainer .footer', {opacity: 0},
          {delay: 50, duration: 200, fill: 'none'}),
    ].flat();
  }

  transitionFromLoadingToCompleteResult(loadingHeight: number): Animation[] {
    const resultsHeight = this.getElement('#resultContainer').offsetHeight;
    return [
      this.fadeOutAndHide('#loading', 'block', {duration: 100}),
      this.fadeIn('#resultContainer', {duration: 100}),

      /* Transition loading height to the full result height, and hide
       * scrollbars while this happens. */
      this.maintainStyles('#body', {overflow: 'hidden'}, {duration: 400}),
      this.animate(
          '#resultContainer',
          [
            {height: `${loadingHeight}px`},
            {height: `${resultsHeight}px`},
          ],
          {duration: 400, easing: STANDARD_EASING}, !isMac),

      this.slideIn('#resultOptions', -32, {duration: 400}),
      this.fadeIn('#resultOptions', {delay: 200, duration: 200}),

      this.slideIn(
          '#resultText', -16,
          {delay: 100, duration: 400, easing: EMPHASIZED_DECELERATE}),
      this.fadeIn('#resultText', {delay: 100, duration: 300}),
      this.animate(
          '#resultText',
          [
            {color: 'var(--compose-result-text-color-while-loading)'},
            {color: 'var(--compose-result-text-color)'},
          ],
          {delay: 400, duration: 100}),

      this.slideIn(
          '#resultFooter', -130,
          {duration: 400, easing: EMPHASIZED_DECELERATE}),
      this.fadeIn('#resultFooter', {delay: 100, duration: 100}),
    ].flat();
  }

  transitionFromPartialToCompleteResult(): Animation[] {
    return this.fadeIn(
        '#resultOptions, #resultFooter', {delay: 100, duration: 100});
  }

  transitionFromResultToLoading(bodyHeight: number, resultsHeight: number):
      Animation[] {
    const loadingHeight = this.getElement('#loading').offsetHeight;
    return [
      this.fadeOutAndHide('#resultContainer', 'flex', {duration: 250}),

      // Fade out result options and keep faded out for the rest of animation.
      this.fadeOut('#resultOptions', {duration: 50}),
      this.maintainStyles(
          '#resultOptions', {opacity: 0},
          {delay: 50, duration: 200, fill: 'none'}),

      this.fadeIn('#loading', {delay: 100, duration: 100}),

      this.animate(
          '#bodyAndFooter',
          [
            {height: `${bodyHeight}px`},
            {height: 'var(--compose-loading-body-and-footer-height)'},
          ],
          {duration: 250, easing: STANDARD_EASING}, !isMac),
      this.animate(
          '#resultContainer',
          [
            {
              height: `${resultsHeight}px`,
              overflow: 'hidden',
              alignItems: 'flex-end',
            },
            {
              height: `${loadingHeight}px`,
              overflow: 'hidden',
              alignItems: 'flex-end',
            },
          ],
          {duration: 250, easing: STANDARD_EASING}, !isMac),
    ].flat();
  }

  transitionFromResultToEditing(resultContainerHeight: number): Animation[] {
    // Keep results body and footer visible while its contents animates out.
    const maintainResultsVisibility = this.maintainStyles(
        '#body, .footer', {
          overflow: 'hidden',
          visibility: 'visible',
        },
        {duration: 200});

    const bodyGapHeightAnimation = this.animate(
        '#body',
        [
          {gap: '8px'},
          {gap: '0px'},
        ],
        {duration: 200, easing: STANDARD_EASING});

    const resultContainerHeightAnimation = this.animate(
        '#resultContainer',
        [
          {
            height: `${resultContainerHeight}px`,
            overflow: 'hidden',
            alignItems: 'flex-end',
          },
          {
            height: '0px',
            overflow: 'hidden',
            alignItems: 'flex-end',
          },
        ],
        {duration: 200, easing: STANDARD_EASING});

    return [
      maintainResultsVisibility,
      bodyGapHeightAnimation,
      resultContainerHeightAnimation,

      // Fade out result UI and keep faded out for the rest of animation.
      this.fadeOut('#resultContainer, #resultFooter', {duration: 100}),
      this.maintainStyles(
          '#resultContainer, #resultFooter', {opacity: 0},
          {delay: 100, duration: 100, fill: 'none'}),

      // Fade in edit form.
      this.fadeIn('#editContainer', {duration: 200}),
      this.fadeIn('#editContainer .footer', {delay: 100, duration: 100}),
    ].flat();
  }

  transitionFromEditingToResult(resultContainerHeight: number): Animation[] {
    const bodyGapHeightAnimation = this.animate(
        '#body',
        [
          {gap: '0px'},
          {gap: '8px'},
        ],
        {duration: 200, easing: STANDARD_EASING});

    const resultContainerHeightAnimation = this.animate(
        '#resultContainer',
        [
          {
            height: '0px',
            overflow: 'hidden',
            alignItems: 'flex-end',
          },
          {
            height: `${resultContainerHeight}px`,
            overflow: 'hidden',
            alignItems: 'flex-end',
          },
        ],
        {duration: 200, easing: STANDARD_EASING});

    return [
      bodyGapHeightAnimation,
      resultContainerHeightAnimation,

      // Fade out edit form.
      this.fadeOutAndHide('#editContainer', 'flex', {duration: 200}),
      this.fadeOutAndHide('#editContainer .footer', 'flex', {duration: 100}),
      this.maintainStyles(
          '#editContainer .footer', {opacity: 0},
          {delay: 100, duration: 100, fill: 'none'}),

      // Fade in result UI.
      this.fadeIn(
          '#resultContainer, #resultFooter', {delay: 100, duration: 100}),
    ].flat();
  }
}