chromium/chrome/browser/resources/lens/overlay/shimmer_circle.ts

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

import {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {CURSOR_STATE_INITIAL_FOCUS_DURATION} from './overlay_shimmer.js';
import {getTemplate} from './shimmer_circle.html.js';
import {Wiggle} from './wiggle.js';

/** The frequency val used in the Wiggle functions. */
export const STEADY_STATE_FREQ_VAL = 0.06;
export const INTERACTION_STATE_FREQ_VAL = 0.03;

/*
 * Controls a single shimmer circle. This class is responsible for controlling
 * properties of an individual circle, that is different on each circle.
 * Shared properties are controlled by the OverlayShimmerElement.
 */
export class ShimmerCircleElement extends PolymerElement {
  static get is() {
    return 'shimmer-circle';
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      colorHex: String,
      steadyStateCenterX: Number,
      steadyStateCenterY: Number,
      isSteadyState: {
        type: Boolean,
        observer: 'isSteadyStateChanged',
      },
      isWiggling: {
        type: Boolean,
        observer: 'isWigglingChanged',
      },
    };
  }

  // The color of this shimmer circle.
  private colorHex: string;
  // The randomized X value that this circle hovers around in the steady state.
  private steadyStateCenterX: number;
  // The randomized Y value that this circle hovers around in the steady state.
  private steadyStateCenterY: number;
  // Whether the circles are focusing the steady state center position.
  private isSteadyState: boolean;
  // Whether the circles are providing randomized movement.
  private isWiggling: boolean;
  // The animation transitioning the center positions to the steady state.
  // Undefined if no steady state animation has played yet.
  private steadyStateAnimation?: Animation;
  // The current time in our wiggle animation.
  private currentWiggleTime = 0;
  // The time in MS of the last requestAnimationFrame call
  private previousAnimationFrameTime = 0;

  // Wiggles.
  private radiusWiggle: Wiggle;
  private centerXWiggle: Wiggle;
  private centerYWiggle: Wiggle;

  constructor() {
    super();

    // Initialize the wiggles
    this.radiusWiggle = new Wiggle(STEADY_STATE_FREQ_VAL);
    this.centerXWiggle = new Wiggle(STEADY_STATE_FREQ_VAL);
    this.centerYWiggle = new Wiggle(STEADY_STATE_FREQ_VAL);
  }

  override connectedCallback() {
    super.connectedCallback();

    // Set this circles color.
    this.style.setProperty('--shimmer-circle-color', this.colorHex);

    // Begin following a randomized motion.
    this.stepRandomMotion(0);
  }

  private isSteadyStateChanged() {
    if (this.isSteadyState) {
      this.focusSteadyState();
    } else {
      this.unfocusSteadyState();
    }
  }

  private isWigglingChanged() {
    if (this.isWiggling) {
      this.startWiggles();
    }
  }

  private async focusSteadyState() {
    this.radiusWiggle.setFrequency(STEADY_STATE_FREQ_VAL);
    this.centerXWiggle.setFrequency(STEADY_STATE_FREQ_VAL);
    this.centerYWiggle.setFrequency(STEADY_STATE_FREQ_VAL);

    const animation = this.animate(
        [
          {
            [`--shimmer-circle-center-x`]: `${this.steadyStateCenterX}%`,
            [`--shimmer-circle-center-y`]: `${this.steadyStateCenterY}%`,
          },
        ],
        {
          duration: 800,
          easing: 'cubic-bezier(0.05, 0.7, 0.1, 1.0)',
          fill: 'forwards',
        });
    this.steadyStateAnimation = animation;
  }

  private unfocusSteadyState() {
    if (this.steadyStateAnimation) {
      // Cancel any current state state animations so they don't override the
      // new inherit properties.
      this.steadyStateAnimation.cancel();
    }

    this.radiusWiggle.setFrequency(INTERACTION_STATE_FREQ_VAL);
    this.centerXWiggle.setFrequency(INTERACTION_STATE_FREQ_VAL);
    this.centerYWiggle.setFrequency(INTERACTION_STATE_FREQ_VAL);

    // Unset the center point so the region to focus is controlled by
    // OverlayShimmerElement. We need to animate this if not there will be an
    // unsightly jump.
    this.animate(
        [
          {
            [`--shimmer-circle-center-x`]: `inherit`,
            [`--shimmer-circle-center-y`]: `inherit`,
          },
        ],
        {
          duration: CURSOR_STATE_INITIAL_FOCUS_DURATION,
          easing: 'cubic-bezier(0.2, 0.0, 0, 1.0)',
          fill: 'forwards',
        });
  }

  private startWiggles() {
    this.stepRandomMotion(this.currentWiggleTime);
  }

  private stepRandomMotion(timeMs: number) {
    this.currentWiggleTime += timeMs - this.previousAnimationFrameTime;
    this.previousAnimationFrameTime = timeMs;
    this.style.setProperty(
        '--shimmer-circle-radius-wiggle',
        this.radiusWiggle.calculateNext(timeMs / 1000).toString());
    this.style.setProperty(
        '--shimmer-circle-center-x-wiggle',
        this.centerXWiggle.calculateNext(timeMs / 1000).toString());
    this.style.setProperty(
        '--shimmer-circle-center-y-wiggle',
        this.centerYWiggle.calculateNext(timeMs / 1000).toString());
    // If not wiggling, stop requesting new animation frames.
    if (this.isWiggling) {
      requestAnimationFrame(this.stepRandomMotion.bind(this));
    }
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'shimmer-circle': ShimmerCircleElement;
  }
}

customElements.define(ShimmerCircleElement.is, ShimmerCircleElement);