chromium/chrome/browser/resources/lens/overlay/wiggle.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.

const INITIAL_ANGULAR_POSITION_MULTIPLIER = 1000;
const BASE_WAVE_AMPLITUDE_FACTOR = 0.53;
const BASE_WAVE_FREQUENCY_FACTOR = 1;
const FIRST_OVERTONE_WAVE_AMPLITUDE_FACTOR = 0.25;
const FIRST_OVERTONE_WAVE_FREQUENCY_FACTOR = 2;
const SECOND_OVERTONE_WAVE_AMPLITUDE_FACTOR = 0.12;
const SECOND_OVERTONE_WAVE_FREQUENCY_FACTOR = 3;

interface WaveParam {
  // Amplitude controls how much the value will change above and below the
  // starting value. For this class, amplitude should be between [-1, 1].
  amplitude: number;
  // Frequency controls how often the value changes per second. A higher
  // frequency makes the value move faster, a lower frequency makes the value
  // move slower.
  frequency: number;
}

function createWaveParam(amplitude: number, frequency: number): WaveParam {
  return {amplitude, frequency};
}

/** Converts frequency to angular frequency (2πf) */
function frequencyToAngularFrequency(frequency: number): number {
  return 2 * Math.PI * frequency;
}

/**
 * Simulates a random "wiggle" motion using the supplied frequency. Values are
 * calculated stepwise, progressing the simulation based on the current time.
 *
 * The calculated values are roughly in the range [-1, 1].
 *
 * For more information see
 * https://www.schoolofmotion.com/blog/wiggle-expression.
 */
export class Wiggle {
  // Randomized constants in pairs of (amplitude, frequency) which make each
  // Wiggle object have a unique path.
  private readonly waveParams = [
    createWaveParam(
        BASE_WAVE_AMPLITUDE_FACTOR * (0.5 + Math.random()),
        BASE_WAVE_FREQUENCY_FACTOR * (0.5 + Math.random()),
        ),
    createWaveParam(
        FIRST_OVERTONE_WAVE_AMPLITUDE_FACTOR * (0.5 + Math.random()),
        FIRST_OVERTONE_WAVE_FREQUENCY_FACTOR * (0.5 + Math.random()),
        ),
    createWaveParam(
        SECOND_OVERTONE_WAVE_AMPLITUDE_FACTOR * (0.5 + Math.random()),
        SECOND_OVERTONE_WAVE_FREQUENCY_FACTOR * (0.5 + Math.random()),
        ),
  ];

  /** Wiggle angular frequency (2πf) */
  private angularFrequency: number;
  /** The internal position of the simulation that is stepped forward */
  private angularPosition: number;
  /** Time in seconds of previous calculation */
  private previousTimeSeconds?: number;
  /** Value of the previous calculated wiggle simulation value. */
  private previousWiggleValue: number;

  constructor(
      frequency: number,
  ) {
    this.angularFrequency = frequencyToAngularFrequency(frequency);
    this.angularPosition = Math.random() * INITIAL_ANGULAR_POSITION_MULTIPLIER;
    // If there was no previous wiggle value (as in the case of entering through
    // the image context menu item), then it is possible for all of the circles
    // to overlap causing their simulated gaussian blur to become more apparent
    // at the gradient color stops. Calculate an initial wiggle value to prevent
    // this.
    this.previousWiggleValue = this.calculateNext(0);
  }

  getPreviousWiggleValue(): number {
    return this.previousWiggleValue;
  }

  setFrequency(frequency: number) {
    this.angularFrequency = frequencyToAngularFrequency(frequency);
  }

  /**
   * Calculates the state of the Wiggle simulation for the current time
   *
   * @param timeSeconds Current simulation time in seconds
   */
  calculateNext(timeSeconds: number): number {
    if (!this.previousTimeSeconds) {
      this.previousTimeSeconds = timeSeconds;
    }
    this.angularPosition +=
        (timeSeconds - this.previousTimeSeconds) * this.angularFrequency;
    this.previousTimeSeconds = timeSeconds;

    let wiggle = 0;
    for (const {amplitude, frequency} of this.waveParams) {
      wiggle += amplitude * Math.sin(frequency * this.angularPosition);
    }
    this.previousWiggleValue = wiggle;
    return wiggle;
  }
}