chromium/chrome/browser/resources/welcome/shared/onboarding_background.ts

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

/**
 * @fileoverview This element contains a set of SVGs that together acts as an
 * animated and responsive background for any page that contains it.
 */

import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import '../strings.m.js';

import {assert} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';

import {getCss} from './onboarding_background.css.js';
import {getHtml} from './onboarding_background.html.js';

export interface OnboardingBackgroundElement {
  $: {
    logo: HTMLElement,
  };
}

export class OnboardingBackgroundElement extends CrLitElement {
  static get is() {
    return 'onboarding-background';
  }

  static override get styles() {
    return getCss();
  }

  override render() {
    return getHtml.bind(this)();
  }

  static override get properties() {
    return {
      forcePaused_: {
        type: Boolean,
        reflect: true,
      },
    };
  }

  private animations_: Animation[] = [];
  private forcePaused_: boolean =
      window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  override connectedCallback() {
    super.connectedCallback();
    const details: Array<[string, number]> = [
      ['blue-line', 60],
      ['green-line', 68],
      ['red-line', 45],
      ['grey-line', 68],
      ['yellow-line', 49],
    ];
    details.forEach(([id, width]) => {
      const element = this.shadowRoot!.querySelector<HTMLElement>(`#${id}`);
      assert(element);
      this.createLineAnimation_(element, width);
    });
  }

  private createLineAnimation_(lineContainer: HTMLElement, width: number) {
    const line = lineContainer.firstElementChild as HTMLElement;
    const lineFill = line.firstElementChild as HTMLElement;
    const pointOptions = {
      endDelay: 3250,
      fill: 'forwards' as FillMode,
      duration: 750,
    };

    const startPointAnimation = lineFill.animate(
        [
          {width: '0px'},
          {width: `${width}px`},
        ],
        Object.assign({}, pointOptions, {easing: 'cubic-bezier(.6,0,0,1)'}));
    startPointAnimation.pause();
    this.animations_.push(startPointAnimation);
    this.loopAnimation_(startPointAnimation);

    const endPointWidthAnimation = line.animate(
        [
          {width: `${width}px`},
          {width: '0px'},
        ],
        Object.assign(
            {}, pointOptions, {easing: 'cubic-bezier(.66,0,.86,.25)'}));
    endPointWidthAnimation.pause();
    this.animations_.push(endPointWidthAnimation);
    this.loopAnimation_(endPointWidthAnimation);

    const endPointTransformAnimation = line.animate(
        [
          {transform: `translateX(0)`},
          {transform: `translateX(${width}px)`},
        ],
        Object.assign({}, pointOptions, {
          easing: 'cubic-bezier(.66,0,.86,.25)',
        }));
    endPointTransformAnimation.pause();
    this.animations_.push(endPointTransformAnimation);
    this.loopAnimation_(endPointTransformAnimation);

    const lineTransformAnimation = lineContainer.animate(
        [
          {transform: `translateX(0)`},
          {transform: `translateX(40px)`},
        ],
        {
          composite: 'add',  // There is already a rotate on the line.
          duration: 1500,
          easing: 'cubic-bezier(0,.56,.46,1)',
          endDelay: 2500,
          fill: 'forwards',
        });
    lineTransformAnimation.pause();
    this.animations_.push(lineTransformAnimation);
    this.loopAnimation_(lineTransformAnimation);
  }

  protected getPlayPauseIcon_(): string {
    return this.forcePaused_ ? 'welcome:play' : 'welcome:pause';
  }

  protected getPlayPauseLabel_(): string {
    return loadTimeData.getString(
        this.forcePaused_ ? 'landingPlayAnimations' : 'landingPauseAnimations');
  }

  private loopAnimation_(animation: Animation) {
    // Animations that have a delay after them can only be looped by re-playing
    // them as soon as they finish. The |endDelay| property of JS animations
    // only works if |iterations| is 1, and the |delay| property runs before
    // the animation even plays.
    animation.onfinish = () => {
      animation.play();
    };
  }

  protected onLogoClick_() {
    this.$.logo.animate(
        {
          transform: [
            'translate(-50%, -50%)',
            'translate(-50%, -50%) rotate(-10turn)',
          ],
        },
        {
          duration: 500,
          easing: 'cubic-bezier(1, 0, 0, 1)',
        });
  }

  protected onPlayPauseClick_() {
    if (this.forcePaused_) {
      this.play();
    } else {
      this.pause();
    }

    this.forcePaused_ = !this.forcePaused_;
  }

  pause() {
    this.animations_.forEach(animation => animation.pause());
  }

  play() {
    if (this.forcePaused_) {
      return;
    }

    this.animations_.forEach(animation => animation.play());
  }
}
customElements.define(
    OnboardingBackgroundElement.is, OnboardingBackgroundElement);