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