// 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();
}
}