modifiers/path/Turbulence.js

import { Particle } from '../../core/Particle.js';
import { ModifierCategory } from '../../shared/Constants.js';

import { Modifier } from '../Modifier.js';

/**
 * @import { ModifierConfig } from './Modifier.js';
 */

/**
 * Turbulence configuration options.
 * Includes all properties from {@link ModifierConfig}.
 * @typedef {object} TurbulenceConfig
 * @property {number|number[]} [strength=20] Magnitude of displacement (in pixels per second) or a [start, end] range array interpolated over lifespan.
 * @property {number} [scale=0.01] Noise scale factor. Smaller values produce smooth, sweeping currents; larger values produce tight, chaotic jitter.
 */

/**
 * Path modifier that applies pseudo-random, continuous displacement to particles over their lifespan.
 * @class
 * @extends Modifier
 */
export class Turbulence extends Modifier {
    // Skipped @override because it fails on static members in TypeScript
    /**
     * Gets the architectural category of the modifier.
     * Used by emitters to sort modifiers into specialized update loops (e.g., visual vs. path).
     * @ignore
     * @type {string}
     * @returns {string}
     */
    static get category() {
        return ModifierCategory.PATH;
    }

    /**
     * Magnitude of displacement (in pixels per second) at particle emission.
     * @type {number}
     */
    startStrength;

    /**
     * Magnitude of displacement (in pixels per second) at particle death.
     * @type {number}
     */
    endStrength;

    /**
     * Noise scale factor controlling the size of turbulence patterns. Smaller values produce smooth, sweeping currents; larger values produce tight, chaotic jitter.
     * @type {number}
     */
    scale;

    /**
     * Initializes a turbulence path modifier.
     * @constructor
     * @param {TurbulenceConfig} [config={}] Turbulence configuration options.
     */
    constructor(config = {}) {
        super(config);

        const strength = config.strength ?? 20;

        if (Array.isArray(strength)) {
            this.startStrength = strength[0];
            this.endStrength = strength[1];
        } else {
            this.startStrength = strength;
            this.endStrength = strength;
        }

        this.scale = config.scale ?? 0.01;
    }

    /**
     * Applies noise-based displacement to the particle's position.
     * @override
     * @param {Particle} particle Particle instance to affect.
     * @param {number} normalizedAge Normalized age of the particle (0.0 = emitted, 1.0 = dead).
     * @param {number} dt Frame time step in seconds.
     * @returns {void}
     */
    update(particle, normalizedAge, dt) {
        const strength = this.startStrength + (this.endStrength - this.startStrength) * normalizedAge;

        if (strength === 0) {
            return;
        }

        const nx = particle.x * this.scale;
        const ny = particle.y * this.scale;
        const time = particle.age;

        const forceX = this.#noise2D(nx, ny, time);
        const forceY = this.#noise2D(ny + 1000, nx + 1000, time);

        particle.x += forceX * strength * dt;
        particle.y += forceY * strength * dt;
    }

    /**
     * Calculates a continuous pseudo-random noise value (-1.0 to 1.0) based on coordinates and time.
     * @param {number} x X coordinate.
     * @param {number} y Y coordinate.
     * @param {number} t Time/age offset.
     * @returns {number} Value in range [-1.0, 1.0].
     */
    #noise2D(x, y, t) {
        // Lightweight trigonometric hash function simulating spatial noise vector fields
        const n = Math.sin(x * 12.9898 + y * 78.233 + t * 43758.5453);

        return (n - Math.floor(n)) * 2 - 1;
    }
}