emitters/EllipseEmitter.js

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

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

/**
 * @import { EmitterConfig } from './Emitter.js'
 */

/**
 * EllipseEmitter configuration options.
 * Includes all properties from {@link EmitterConfig}.
 * @typedef {object} EllipseEmitterConfig
 * @property {number} [x=0] Horizontal coordinate of the emission ellipse center.
 * @property {number} [y=0] Vertical coordinate of the emission ellipse center.
 * @property {number} [radiusX=50] Horizontal radius of the emission ellipse.
 * @property {number} [radiusY=50] Vertical radius of the emission ellipse.
 */

/**
 * Particle emitter that emits particles randomly from an elliptical area, using a uniform distribution.
 * @class
 * @extends Emitter
 */
export class EllipseEmitter extends Emitter {
    /**
     * Horizontal coordinate of the emission ellipse center.
     * @type {number}
     */
    x;

    /**
     * Vertical coordinate of the emission ellipse center.
     * @type {number}
     */
    y;

    /**
     * Horizontal radius of the emission ellipse.
     * @type {number}
     */
    radiusX;

    /**
     * Vertical radius of the emission ellipse.
     * @type {number}
     */
    radiusY;

    /**
     * Initializes an ellipse emitter with a given position and radius.
     * Particles are emitted randomly from the elliptical area using a uniform distribution.
     * @constructor
     * @param {EllipseEmitterConfig} [config={}] EllipseEmitter configuration options.
     */
    constructor(config = {}) {
        super(config);

        this.x = config.x ?? 0;
        this.y = config.y ?? 0;
        this.radiusX = config.radiusX ?? 50;
        this.radiusY = config.radiusY ?? 50;
    }

    /**
     * Extends the base initialization by positioning the particle at a random point along or within the ellipse.
     * @override
     * @param {Particle} particle Particle instance to initialize.
     * @returns {void}
     */
    _initParticle(particle) {
        const angle = Math.random() * Math.PI * 2;

        const factor = this.emissionSource === EmissionSource.VOLUME
            ? Math.sqrt(Math.random())
            : 1;

        particle.x = this.x + Math.cos(angle) * this.radiusX * factor;
        particle.y = this.y + Math.sin(angle) * this.radiusY * factor;

        super._initParticle(particle);
    }

    /**
     * Calculates the default emission direction angle based on the emitter geometry and emission source mode.
     * @override
     * @param {Particle} particle Particle instance used to calculate the direction from its spawn position.
     * @returns {number} The default emission direction angle (in radians).
     */
    _getDefaultDirection(particle) {
        const dy = particle.y - this.y;
        const dx = particle.x - this.x;

        // If a particle spawns at the center, fallback to a random direction
        const outwardAngle = (dx === 0 && dy === 0)
            ? Math.random() * Math.PI * 2
            : Math.atan2(dy, dx);

        switch (this.emissionSource) {
            case EmissionSource.EDGE_OUT:
                return outwardAngle;
            case EmissionSource.EDGE_IN:
                return outwardAngle + Math.PI;
            case EmissionSource.EDGE_BOTH:
                return Math.random() > 0.5 ? outwardAngle : outwardAngle + Math.PI;
            case EmissionSource.VOLUME:
            default:
                return outwardAngle;
        }
    }
}