'use strict';

/**
 * @file ParticleSystem2.ts
 * @description Advanced Particle System with GPU Compute and Visual Editor
 * PRO-1.4: Professional Editor Features
 *
 * Features:
 * - GPU-accelerated particles (WebGPU Compute)
 * - Multiple emitter shapes
 * - Force fields (gravity, wind, vortex, etc.)
 * - Sub-emitters and trails
 * - Collision detection
 * - LOD system
 * - Visual editor integration
 */
// ============================================================
// Particle System Class
// ============================================================
class ParticleSystem2 {
    constructor(def) {
        this.useGPU = false;
        this.definition = def;
        this.state = this.createInitialState();
    }
    createInitialState() {
        const maxParticles = this.definition.main.maxParticles;
        const particles = [];
        for (let i = 0; i < maxParticles; i++) {
            particles.push(this.createDeadParticle());
        }
        return {
            particles,
            activeCount: 0,
            time: 0,
            isPlaying: false,
            isPaused: false,
            isStopped: true,
            isEmitting: false,
            randomSeed: (Math.random() * 0xffffffff) | 0,
            emitCounter: 0,
            distanceTraveled: 0,
            lastPosition: new Float32Array([0, 0, 0]),
            burstStates: this.definition.emission.bursts.map((b, i) => ({
                burstIndex: i,
                cycleCount: 0,
                nextTime: b.time,
            })),
        };
    }
    createDeadParticle() {
        return {
            position: new Float32Array([0, 0, 0]),
            velocity: new Float32Array([0, 0, 0]),
            startColor: new Float32Array([1, 1, 1, 1]),
            color: new Float32Array([1, 1, 1, 1]),
            startSize: new Float32Array([1, 1, 1]),
            size: new Float32Array([1, 1, 1]),
            rotation: new Float32Array([0, 0, 0]),
            angularVelocity: new Float32Array([0, 0, 0]),
            startLifetime: 0,
            remainingLifetime: -1,
            normalizedLifetime: 1,
            randomSeed: (Math.random() * 0xffffffff) | 0,
            startSpeed: 0,
            axisOfRotation: new Float32Array([0, 0, 1]),
            meshIndex: 0,
            customData1: new Float32Array([0, 0, 0, 0]),
            customData2: new Float32Array([0, 0, 0, 0]),
        };
    }
    // ─── Playback Control ──────────────────────────────────
    play() {
        if (this.state.isPaused) {
            this.state.isPaused = false;
        }
        this.state.isPlaying = true;
        this.state.isStopped = false;
        this.state.isEmitting = true;
        if (this.definition.main.prewarm) {
            this.prewarm();
        }
    }
    pause() {
        this.state.isPaused = true;
    }
    stop(clear = true) {
        this.state.isPlaying = false;
        this.state.isStopped = true;
        this.state.isEmitting = false;
        if (clear) {
            this.clear();
        }
    }
    clear() {
        this.state.activeCount = 0;
        this.state.time = 0;
        this.state.emitCounter = 0;
        this.state.distanceTraveled = 0;
        for (const particle of this.state.particles) {
            particle.remainingLifetime = -1;
        }
        this.resetBurstStates();
    }
    resetBurstStates() {
        this.state.burstStates = this.definition.emission.bursts.map((b, i) => ({
            burstIndex: i,
            cycleCount: 0,
            nextTime: b.time,
        }));
    }
    prewarm() {
        const duration = this.definition.main.duration;
        const dt = 1 / 60;
        const steps = Math.ceil(duration / dt);
        for (let i = 0; i < steps; i++) {
            this.simulate(dt);
        }
    }
    // ─── Emission ──────────────────────────────────────────
    emit(count, position) {
        for (let i = 0; i < count; i++) {
            this.emitSingleParticle(position);
        }
    }
    emitSingleParticle(overridePosition) {
        // Find dead particle
        const deadIndex = this.findDeadParticleIndex();
        if (deadIndex === -1)
            return false;
        const particle = this.state.particles[deadIndex];
        this.initializeParticle(particle, overridePosition);
        this.state.activeCount++;
        return true;
    }
    findDeadParticleIndex() {
        for (let i = 0; i < this.state.particles.length; i++) {
            if (this.state.particles[i].remainingLifetime < 0) {
                return i;
            }
        }
        return -1;
    }
    initializeParticle(particle, overridePosition) {
        const main = this.definition.main;
        // Random seed for this particle
        particle.randomSeed = (Math.random() * 0xffffffff) | 0;
        // Position and velocity from shape
        const { position, velocity } = this.sampleEmitterShape();
        if (overridePosition) {
            particle.position.set(overridePosition);
        }
        else {
            particle.position.set(position);
        }
        // Initial velocity
        const startSpeed = this.evaluateMinMaxCurve(main.startSpeed, 0, particle.randomSeed);
        particle.startSpeed = startSpeed;
        particle.velocity[0] = velocity[0] * startSpeed;
        particle.velocity[1] = velocity[1] * startSpeed;
        particle.velocity[2] = velocity[2] * startSpeed;
        // Lifetime
        particle.startLifetime = this.evaluateMinMaxCurve(main.startLifetime, 0, particle.randomSeed);
        particle.remainingLifetime = particle.startLifetime;
        particle.normalizedLifetime = 0;
        // Size
        if (main.startSize3D) {
            particle.startSize[0] = this.evaluateMinMaxCurve(main.startSizeX, 0, particle.randomSeed);
            particle.startSize[1] = this.evaluateMinMaxCurve(main.startSizeY, 0, particle.randomSeed);
            particle.startSize[2] = this.evaluateMinMaxCurve(main.startSizeZ, 0, particle.randomSeed);
        }
        else {
            const size = this.evaluateMinMaxCurve(main.startSize, 0, particle.randomSeed);
            particle.startSize.set([size, size, size]);
        }
        particle.size.set(particle.startSize);
        // Rotation
        if (main.startRotation3D) {
            particle.rotation[0] = this.evaluateMinMaxCurve(main.startRotationX, 0, particle.randomSeed);
            particle.rotation[1] = this.evaluateMinMaxCurve(main.startRotationY, 0, particle.randomSeed);
            particle.rotation[2] = this.evaluateMinMaxCurve(main.startRotationZ, 0, particle.randomSeed);
        }
        else {
            particle.rotation[2] = this.evaluateMinMaxCurve(main.startRotation, 0, particle.randomSeed);
        }
        // Flip rotation
        if (main.flipRotation > 0 && Math.random() < main.flipRotation) {
            particle.rotation[2] += Math.PI;
        }
        // Color
        const color = this.evaluateMinMaxGradient(main.startColor, 0, particle.randomSeed);
        particle.startColor.set(color);
        particle.color.set(color);
        // Angular velocity (will be set by rotationOverLifetime)
        particle.angularVelocity.set([0, 0, 0]);
    }
    sampleEmitterShape() {
        const shape = this.definition.shape;
        switch (shape.type) {
            case 'sphere':
                return this.sampleSphere(shape);
            case 'hemisphere':
                return this.sampleHemisphere(shape);
            case 'cone':
                return this.sampleCone(shape);
            case 'box':
                return this.sampleBox(shape);
            case 'circle':
                return this.sampleCircle(shape);
            default:
                return { position: [0, 0, 0], velocity: [0, 1, 0] };
        }
    }
    sampleSphere(shape) {
        // Random direction
        const theta = Math.random() * 2 * Math.PI;
        const phi = Math.acos(2 * Math.random() - 1);
        const sinPhi = Math.sin(phi);
        const dir = [sinPhi * Math.cos(theta), sinPhi * Math.sin(theta), Math.cos(phi)];
        // Position on/in sphere
        const radiusFactor = shape.radiusThickness === 1
            ? Math.cbrt(Math.random())
            : 1 - Math.random() * shape.radiusThickness;
        const r = shape.radius * radiusFactor;
        const position = [
            dir[0] * r + shape.position[0],
            dir[1] * r + shape.position[1],
            dir[2] * r + shape.position[2],
        ];
        return { position, velocity: dir };
    }
    sampleHemisphere(shape) {
        const theta = Math.random() * 2 * Math.PI;
        const phi = Math.acos(Math.random()); // 0 to PI/2
        const sinPhi = Math.sin(phi);
        const dir = [sinPhi * Math.cos(theta), Math.cos(phi), sinPhi * Math.sin(theta)];
        const radiusFactor = shape.radiusThickness === 1
            ? Math.cbrt(Math.random())
            : 1 - Math.random() * shape.radiusThickness;
        const r = shape.radius * radiusFactor;
        const position = [
            dir[0] * r + shape.position[0],
            dir[1] * r + shape.position[1],
            dir[2] * r + shape.position[2],
        ];
        return { position, velocity: dir };
    }
    sampleCone(shape) {
        const angleRad = (shape.angle * Math.PI) / 180;
        const theta = Math.random() * 2 * Math.PI;
        // Random angle within cone
        const coneAngle = Math.random() * angleRad;
        const sinAngle = Math.sin(coneAngle);
        const cosAngle = Math.cos(coneAngle);
        const dir = [sinAngle * Math.cos(theta), cosAngle, sinAngle * Math.sin(theta)];
        // Position based on emitFrom
        let position;
        switch (shape.emitFrom) {
            case 'base':
                const r = shape.radius * Math.sqrt(Math.random());
                position = [
                    r * Math.cos(theta) + shape.position[0],
                    shape.position[1],
                    r * Math.sin(theta) + shape.position[2],
                ];
                break;
            case 'volume':
                const height = Math.random() * shape.length;
                const radiusAtHeight = shape.radius + height * Math.tan(angleRad);
                const rV = radiusAtHeight * Math.sqrt(Math.random());
                position = [
                    rV * Math.cos(theta) + shape.position[0],
                    height + shape.position[1],
                    rV * Math.sin(theta) + shape.position[2],
                ];
                break;
            default:
                position = [...shape.position];
        }
        return { position, velocity: dir };
    }
    sampleBox(shape) {
        const hw = shape.scale[0] / 2;
        const hh = shape.scale[1] / 2;
        const hd = shape.scale[2] / 2;
        let position;
        switch (shape.emitFrom) {
            case 'volume':
                position = [
                    (Math.random() * 2 - 1) * hw + shape.position[0],
                    (Math.random() * 2 - 1) * hh + shape.position[1],
                    (Math.random() * 2 - 1) * hd + shape.position[2],
                ];
                break;
            case 'shell':
                // Random face
                const face = Math.floor(Math.random() * 6);
                const u = Math.random() * 2 - 1;
                const v = Math.random() * 2 - 1;
                switch (face) {
                    case 0:
                        position = [hw, u * hh, v * hd];
                        break;
                    case 1:
                        position = [-hw, u * hh, v * hd];
                        break;
                    case 2:
                        position = [u * hw, hh, v * hd];
                        break;
                    case 3:
                        position = [u * hw, -hh, v * hd];
                        break;
                    case 4:
                        position = [u * hw, v * hh, hd];
                        break;
                    default:
                        position = [u * hw, v * hh, -hd];
                        break;
                }
                position[0] += shape.position[0];
                position[1] += shape.position[1];
                position[2] += shape.position[2];
                break;
            default:
                position = [...shape.position];
        }
        return { position, velocity: [0, 1, 0] };
    }
    sampleCircle(shape) {
        const theta = Math.random() * 2 * Math.PI;
        const radiusFactor = shape.radiusThickness === 1
            ? Math.sqrt(Math.random())
            : 1 - Math.random() * shape.radiusThickness;
        const r = shape.radius * radiusFactor;
        const position = [
            r * Math.cos(theta) + shape.position[0],
            shape.position[1],
            r * Math.sin(theta) + shape.position[2],
        ];
        return { position, velocity: [0, 1, 0] };
    }
    // ─── Simulation ────────────────────────────────────────
    simulate(deltaTime) {
        if (!this.state.isPlaying || this.state.isPaused)
            return;
        const dt = deltaTime * this.definition.main.simulationSpeed;
        // Update time
        this.state.time += dt;
        // Handle looping
        if (this.definition.main.looping && this.state.time >= this.definition.main.duration) {
            this.state.time = this.state.time % this.definition.main.duration;
            this.resetBurstStates();
        }
        // Emit particles
        if (this.state.isEmitting) {
            this.processEmission(dt);
        }
        // Update particles
        for (let i = 0; i < this.state.particles.length; i++) {
            const particle = this.state.particles[i];
            if (particle.remainingLifetime >= 0) {
                this.updateParticle(particle, dt);
            }
        }
    }
    processEmission(dt) {
        const emission = this.definition.emission;
        if (!emission.enabled)
            return;
        // Rate over time
        const rateOverTime = this.evaluateMinMaxCurve(emission.rateOverTime, this.normalizedTime(), 0);
        this.state.emitCounter += rateOverTime * dt;
        while (this.state.emitCounter >= 1) {
            this.emitSingleParticle();
            this.state.emitCounter -= 1;
        }
        // Bursts
        for (const burstState of this.state.burstStates) {
            const burst = emission.bursts[burstState.burstIndex];
            while (this.state.time >= burstState.nextTime && burstState.cycleCount < burst.cycles) {
                // Probability check
                if (Math.random() < burst.probability) {
                    const count = Math.round(this.evaluateMinMaxCurve(burst.count, 0, (Math.random() * 0xffffffff) | 0));
                    this.emit(count);
                }
                burstState.cycleCount++;
                burstState.nextTime += burst.interval;
            }
        }
    }
    updateParticle(particle, dt) {
        // Update lifetime
        particle.remainingLifetime -= dt;
        if (particle.remainingLifetime < 0) {
            // Particle died
            this.state.activeCount--;
            // Trigger sub-emitter death
            this.triggerSubEmitters('death', particle);
            return;
        }
        particle.normalizedLifetime = 1 - particle.remainingLifetime / particle.startLifetime;
        // Apply forces
        this.applyForces(particle, dt);
        // Apply velocity/force over lifetime modules
        this.applyVelocityOverLifetime(particle, dt);
        this.applyForceOverLifetime(particle, dt);
        this.applyLimitVelocityOverLifetime(particle, dt);
        // Integrate position
        particle.position[0] += particle.velocity[0] * dt;
        particle.position[1] += particle.velocity[1] * dt;
        particle.position[2] += particle.velocity[2] * dt;
        // Apply noise
        this.applyNoise(particle, dt);
        // Update visual properties
        this.applyColorOverLifetime(particle);
        this.applySizeOverLifetime(particle);
        this.applyRotationOverLifetime(particle, dt);
        // Speed-based modifications
        this.applyColorBySpeed(particle);
        this.applySizeBySpeed(particle);
        this.applyRotationBySpeed(particle, dt);
        // Handle collision
        if (this.definition.collision.enabled) {
            this.handleCollision(particle);
        }
    }
    applyForces(particle, dt) {
        const main = this.definition.main;
        // Gravity
        const gravity = this.evaluateMinMaxCurve(main.gravityModifier, particle.normalizedLifetime, particle.randomSeed);
        particle.velocity[1] -= 9.81 * gravity * dt;
        // External force fields
        if (this.definition.externalForces.enabled) {
            for (const field of this.definition.externalForces.forceFields) {
                this.applyForceField(particle, field, dt);
            }
        }
    }
    applyForceField(particle, field, dt) {
        switch (field.type) {
            case 'gravity':
                const strength = field.strength;
                particle.velocity[0] += field.direction[0] * strength * dt;
                particle.velocity[1] += field.direction[1] * strength * dt;
                particle.velocity[2] += field.direction[2] * strength * dt;
                break;
            case 'directional':
                const dirStrength = this.evaluateMinMaxCurve(field.strength, particle.normalizedLifetime, particle.randomSeed);
                particle.velocity[0] += field.direction[0] * dirStrength * dt;
                particle.velocity[1] += field.direction[1] * dirStrength * dt;
                particle.velocity[2] += field.direction[2] * dirStrength * dt;
                break;
            case 'vortex':
                const vortexStrength = this.evaluateMinMaxCurve(field.strength, particle.normalizedLifetime, particle.randomSeed);
                const toCenter = [
                    field.center[0] - particle.position[0],
                    field.center[1] - particle.position[1],
                    field.center[2] - particle.position[2],
                ];
                const dist = Math.sqrt(toCenter[0] ** 2 + toCenter[1] ** 2 + toCenter[2] ** 2);
                if (dist > 0.001 && dist < field.radius) {
                    // Tangential force (cross product with axis)
                    const tangent = [
                        field.axis[1] * toCenter[2] - field.axis[2] * toCenter[1],
                        field.axis[2] * toCenter[0] - field.axis[0] * toCenter[2],
                        field.axis[0] * toCenter[1] - field.axis[1] * toCenter[0],
                    ];
                    const tangentLen = Math.sqrt(tangent[0] ** 2 + tangent[1] ** 2 + tangent[2] ** 2);
                    if (tangentLen > 0.001) {
                        const factor = (vortexStrength * (1 - dist / field.radius)) / tangentLen;
                        particle.velocity[0] += tangent[0] * factor * dt;
                        particle.velocity[1] += tangent[1] * factor * dt;
                        particle.velocity[2] += tangent[2] * factor * dt;
                    }
                    // Attraction
                    if (field.attractionStrength > 0) {
                        const attractFactor = (field.attractionStrength * (1 - dist / field.radius)) / dist;
                        particle.velocity[0] += toCenter[0] * attractFactor * dt;
                        particle.velocity[1] += toCenter[1] * attractFactor * dt;
                        particle.velocity[2] += toCenter[2] * attractFactor * dt;
                    }
                }
                break;
            case 'drag':
                const dragCoeff = this.evaluateMinMaxCurve(field.coefficient, particle.normalizedLifetime, particle.randomSeed);
                let drag = dragCoeff;
                if (field.multiplyBySize) {
                    const sizeMag = Math.sqrt(particle.size[0] ** 2 + particle.size[1] ** 2 + particle.size[2] ** 2);
                    drag *= sizeMag;
                }
                if (field.multiplyByVelocity) {
                    const velMag = Math.sqrt(particle.velocity[0] ** 2 + particle.velocity[1] ** 2 + particle.velocity[2] ** 2);
                    drag *= velMag;
                }
                const factor = Math.max(0, 1 - drag * dt);
                particle.velocity[0] *= factor;
                particle.velocity[1] *= factor;
                particle.velocity[2] *= factor;
                break;
            case 'attractor':
                const attractorStrength = this.evaluateMinMaxCurve(field.strength, particle.normalizedLifetime, particle.randomSeed);
                const toAttractor = [
                    field.position[0] - particle.position[0],
                    field.position[1] - particle.position[1],
                    field.position[2] - particle.position[2],
                ];
                const attractorDist = Math.sqrt(toAttractor[0] ** 2 + toAttractor[1] ** 2 + toAttractor[2] ** 2);
                if (attractorDist > 0.001 && attractorDist < field.radius) {
                    const falloff = 1 - attractorDist / field.radius;
                    const force = (attractorStrength * falloff) / attractorDist;
                    particle.velocity[0] += toAttractor[0] * force * dt;
                    particle.velocity[1] += toAttractor[1] * force * dt;
                    particle.velocity[2] += toAttractor[2] * force * dt;
                    if (field.killOnArrive && attractorDist < 0.1) {
                        particle.remainingLifetime = -1;
                    }
                }
                break;
        }
    }
    applyVelocityOverLifetime(particle, dt) {
        const vol = this.definition.velocityOverLifetime;
        if (!vol.enabled)
            return;
        // Linear velocity
        if (vol.linear.mode !== 'constant' ||
            vol.linear.value[0] !== 0 ||
            vol.linear.value[1] !== 0 ||
            vol.linear.value[2] !== 0) {
            const linear = this.evaluateMinMaxVector3(vol.linear, particle.normalizedLifetime, particle.randomSeed);
            particle.velocity[0] += linear[0] * dt;
            particle.velocity[1] += linear[1] * dt;
            particle.velocity[2] += linear[2] * dt;
        }
        // Radial velocity
        const radial = this.evaluateMinMaxCurve(vol.radial, particle.normalizedLifetime, particle.randomSeed);
        if (radial !== 0) {
            const len = Math.sqrt(particle.position[0] ** 2 + particle.position[1] ** 2 + particle.position[2] ** 2);
            if (len > 0.001) {
                particle.velocity[0] += (particle.position[0] / len) * radial * dt;
                particle.velocity[1] += (particle.position[1] / len) * radial * dt;
                particle.velocity[2] += (particle.position[2] / len) * radial * dt;
            }
        }
        // Speed modifier
        const speedMod = this.evaluateMinMaxCurve(vol.speedModifier, particle.normalizedLifetime, particle.randomSeed);
        particle.velocity[0] *= speedMod;
        particle.velocity[1] *= speedMod;
        particle.velocity[2] *= speedMod;
    }
    applyForceOverLifetime(particle, dt) {
        const fol = this.definition.forceOverLifetime;
        if (!fol.enabled)
            return;
        const seed = fol.randomize ? (Math.random() * 0xffffffff) | 0 : particle.randomSeed;
        particle.velocity[0] += this.evaluateMinMaxCurve(fol.x, particle.normalizedLifetime, seed) * dt;
        particle.velocity[1] += this.evaluateMinMaxCurve(fol.y, particle.normalizedLifetime, seed) * dt;
        particle.velocity[2] += this.evaluateMinMaxCurve(fol.z, particle.normalizedLifetime, seed) * dt;
    }
    applyLimitVelocityOverLifetime(particle, dt) {
        const lvol = this.definition.limitVelocityOverLifetime;
        if (!lvol.enabled)
            return;
        const velMag = Math.sqrt(particle.velocity[0] ** 2 + particle.velocity[1] ** 2 + particle.velocity[2] ** 2);
        let maxSpeed;
        if (lvol.separateAxes) {
            // Per-axis limit (simplified)
            maxSpeed = this.evaluateMinMaxCurve(lvol.speed, particle.normalizedLifetime, particle.randomSeed);
        }
        else {
            maxSpeed = this.evaluateMinMaxCurve(lvol.speed, particle.normalizedLifetime, particle.randomSeed);
        }
        if (velMag > maxSpeed) {
            const dampenFactor = 1 - lvol.dampen;
            const targetSpeed = maxSpeed + (velMag - maxSpeed) * dampenFactor;
            const scale = targetSpeed / velMag;
            particle.velocity[0] *= scale;
            particle.velocity[1] *= scale;
            particle.velocity[2] *= scale;
        }
        // Drag
        const drag = this.evaluateMinMaxCurve(lvol.drag, particle.normalizedLifetime, particle.randomSeed);
        if (drag > 0) {
            const dragFactor = Math.max(0, 1 - drag * dt);
            particle.velocity[0] *= dragFactor;
            particle.velocity[1] *= dragFactor;
            particle.velocity[2] *= dragFactor;
        }
    }
    applyNoise(particle, dt) {
        const noise = this.definition.noise;
        if (!noise.enabled)
            return;
        const strength = this.evaluateMinMaxCurve(noise.strength, particle.normalizedLifetime, particle.randomSeed);
        const freq = noise.frequency;
        const scroll = noise.scrollSpeed * this.state.time;
        // Simple noise approximation
        const nx = particle.position[0] * freq + scroll;
        const ny = particle.position[1] * freq + scroll;
        const nz = particle.position[2] * freq + scroll;
        // Pseudo-noise
        const noiseX = (Math.sin(nx * 12.9898 + ny * 78.233) * 43758.5453) % 1;
        const noiseY = (Math.sin(ny * 12.9898 + nz * 78.233) * 43758.5453) % 1;
        const noiseZ = (Math.sin(nz * 12.9898 + nx * 78.233) * 43758.5453) % 1;
        const posAmount = this.evaluateMinMaxCurve(noise.positionAmount, particle.normalizedLifetime, particle.randomSeed);
        particle.position[0] += (noiseX * 2 - 1) * strength * posAmount * dt;
        particle.position[1] += (noiseY * 2 - 1) * strength * posAmount * dt;
        particle.position[2] += (noiseZ * 2 - 1) * strength * posAmount * dt;
    }
    applyColorOverLifetime(particle) {
        const col = this.definition.colorOverLifetime;
        if (!col.enabled)
            return;
        const color = this.evaluateMinMaxGradient(col.color, particle.normalizedLifetime, particle.randomSeed);
        // Multiply with start color
        particle.color[0] = particle.startColor[0] * color[0];
        particle.color[1] = particle.startColor[1] * color[1];
        particle.color[2] = particle.startColor[2] * color[2];
        particle.color[3] = particle.startColor[3] * color[3];
    }
    applySizeOverLifetime(particle) {
        const sol = this.definition.sizeOverLifetime;
        if (!sol.enabled)
            return;
        if (sol.separateAxes) {
            particle.size[0] =
                particle.startSize[0] *
                    this.evaluateMinMaxCurve(sol.sizeX, particle.normalizedLifetime, particle.randomSeed);
            particle.size[1] =
                particle.startSize[1] *
                    this.evaluateMinMaxCurve(sol.sizeY, particle.normalizedLifetime, particle.randomSeed);
            particle.size[2] =
                particle.startSize[2] *
                    this.evaluateMinMaxCurve(sol.sizeZ, particle.normalizedLifetime, particle.randomSeed);
        }
        else {
            const scale = this.evaluateMinMaxCurve(sol.size, particle.normalizedLifetime, particle.randomSeed);
            particle.size[0] = particle.startSize[0] * scale;
            particle.size[1] = particle.startSize[1] * scale;
            particle.size[2] = particle.startSize[2] * scale;
        }
    }
    applyRotationOverLifetime(particle, dt) {
        const rol = this.definition.rotationOverLifetime;
        if (!rol.enabled)
            return;
        if (rol.separateAxes) {
            particle.rotation[0] +=
                this.evaluateMinMaxCurve(rol.angularVelocityX, particle.normalizedLifetime, particle.randomSeed) * dt;
            particle.rotation[1] +=
                this.evaluateMinMaxCurve(rol.angularVelocityY, particle.normalizedLifetime, particle.randomSeed) * dt;
            particle.rotation[2] +=
                this.evaluateMinMaxCurve(rol.angularVelocityZ, particle.normalizedLifetime, particle.randomSeed) * dt;
        }
        else {
            particle.rotation[2] +=
                this.evaluateMinMaxCurve(rol.angularVelocity, particle.normalizedLifetime, particle.randomSeed) * dt;
        }
    }
    applyColorBySpeed(particle) {
        const cbs = this.definition.colorBySpeed;
        if (!cbs.enabled)
            return;
        const speed = Math.sqrt(particle.velocity[0] ** 2 + particle.velocity[1] ** 2 + particle.velocity[2] ** 2);
        const t = Math.min(1, Math.max(0, (speed - cbs.speedRange[0]) / (cbs.speedRange[1] - cbs.speedRange[0])));
        const color = this.evaluateMinMaxGradient(cbs.color, t, particle.randomSeed);
        particle.color[0] *= color[0];
        particle.color[1] *= color[1];
        particle.color[2] *= color[2];
        particle.color[3] *= color[3];
    }
    applySizeBySpeed(particle) {
        const sbs = this.definition.sizeBySpeed;
        if (!sbs.enabled)
            return;
        const speed = Math.sqrt(particle.velocity[0] ** 2 + particle.velocity[1] ** 2 + particle.velocity[2] ** 2);
        const t = Math.min(1, Math.max(0, (speed - sbs.speedRange[0]) / (sbs.speedRange[1] - sbs.speedRange[0])));
        if (sbs.separateAxes) {
            particle.size[0] *= this.evaluateMinMaxCurve(sbs.sizeX, t, particle.randomSeed);
            particle.size[1] *= this.evaluateMinMaxCurve(sbs.sizeY, t, particle.randomSeed);
            particle.size[2] *= this.evaluateMinMaxCurve(sbs.sizeZ, t, particle.randomSeed);
        }
        else {
            const scale = this.evaluateMinMaxCurve(sbs.size, t, particle.randomSeed);
            particle.size[0] *= scale;
            particle.size[1] *= scale;
            particle.size[2] *= scale;
        }
    }
    applyRotationBySpeed(particle, dt) {
        const rbs = this.definition.rotationBySpeed;
        if (!rbs.enabled)
            return;
        const speed = Math.sqrt(particle.velocity[0] ** 2 + particle.velocity[1] ** 2 + particle.velocity[2] ** 2);
        const t = Math.min(1, Math.max(0, (speed - rbs.speedRange[0]) / (rbs.speedRange[1] - rbs.speedRange[0])));
        if (rbs.separateAxes) {
            particle.rotation[0] +=
                this.evaluateMinMaxCurve(rbs.angularVelocityX, t, particle.randomSeed) * dt;
            particle.rotation[1] +=
                this.evaluateMinMaxCurve(rbs.angularVelocityY, t, particle.randomSeed) * dt;
            particle.rotation[2] +=
                this.evaluateMinMaxCurve(rbs.angularVelocityZ, t, particle.randomSeed) * dt;
        }
        else {
            particle.rotation[2] +=
                this.evaluateMinMaxCurve(rbs.angularVelocity, t, particle.randomSeed) * dt;
        }
    }
    handleCollision(particle) {
        const collision = this.definition.collision;
        if (collision.type === 'planes') {
            for (const plane of collision.planes) {
                const dot = particle.position[0] * plane.normal[0] +
                    particle.position[1] * plane.normal[1] +
                    particle.position[2] * plane.normal[2];
                const planeDist = plane.position[0] * plane.normal[0] +
                    plane.position[1] * plane.normal[1] +
                    plane.position[2] * plane.normal[2];
                if (dot < planeDist) {
                    // Below plane - collision occurred
                    const velDotN = particle.velocity[0] * plane.normal[0] +
                        particle.velocity[1] * plane.normal[1] +
                        particle.velocity[2] * plane.normal[2];
                    const bounce = this.evaluateMinMaxCurve(collision.bounce, particle.normalizedLifetime, particle.randomSeed);
                    const dampen = this.evaluateMinMaxCurve(collision.dampen, particle.normalizedLifetime, particle.randomSeed);
                    // Reflect velocity
                    particle.velocity[0] -= (1 + bounce) * velDotN * plane.normal[0];
                    particle.velocity[1] -= (1 + bounce) * velDotN * plane.normal[1];
                    particle.velocity[2] -= (1 + bounce) * velDotN * plane.normal[2];
                    // Apply damping
                    particle.velocity[0] *= 1 - dampen;
                    particle.velocity[1] *= 1 - dampen;
                    particle.velocity[2] *= 1 - dampen;
                    // Move back to plane
                    particle.position[0] += (planeDist - dot) * plane.normal[0];
                    particle.position[1] += (planeDist - dot) * plane.normal[1];
                    particle.position[2] += (planeDist - dot) * plane.normal[2];
                    // Lifetime loss
                    const lifetimeLoss = this.evaluateMinMaxCurve(collision.lifetimeLoss, particle.normalizedLifetime, particle.randomSeed);
                    particle.remainingLifetime -= particle.startLifetime * lifetimeLoss;
                    // Kill if too slow
                    const speed = Math.sqrt(particle.velocity[0] ** 2 + particle.velocity[1] ** 2 + particle.velocity[2] ** 2);
                    if (speed < collision.minKillSpeed) {
                        particle.remainingLifetime = -1;
                    }
                    // Trigger sub-emitter
                    this.triggerSubEmitters('collision', particle);
                }
            }
        }
    }
    triggerSubEmitters(trigger, particle) {
        for (const subEmitter of this.definition.subEmitters) {
            if (subEmitter.trigger === trigger && Math.random() < subEmitter.probability) ;
        }
    }
    // ─── Curve/Gradient Evaluation ─────────────────────────
    evaluateMinMaxCurve(curve, t, seed) {
        switch (curve.mode) {
            case 'constant':
                return curve.value;
            case 'curve':
                return this.evaluateAnimationCurve(curve.curve, t) * curve.multiplier;
            case 'randomBetweenTwoConstants':
                const rand = this.seededRandom(seed);
                return curve.min + (curve.max - curve.min) * rand;
            case 'randomBetweenTwoCurves':
                const randCurve = this.seededRandom(seed);
                const minVal = this.evaluateAnimationCurve(curve.curveMin, t);
                const maxVal = this.evaluateAnimationCurve(curve.curveMax, t);
                return (minVal + (maxVal - minVal) * randCurve) * curve.multiplier;
        }
    }
    evaluateAnimationCurve(curve, t) {
        const keyframes = curve.keyframes;
        if (keyframes.length === 0)
            return 0;
        if (keyframes.length === 1)
            return keyframes[0].value;
        // Handle wrap modes
        if (t < 0)
            t = this.handleWrapMode(t, curve.preWrapMode);
        if (t > 1)
            t = this.handleWrapMode(t, curve.postWrapMode);
        // Find keyframes
        for (let i = 0; i < keyframes.length - 1; i++) {
            const k0 = keyframes[i];
            const k1 = keyframes[i + 1];
            if (t >= k0.time && t <= k1.time) {
                const localT = (t - k0.time) / (k1.time - k0.time);
                // Hermite interpolation
                const t2 = localT * localT;
                const t3 = t2 * localT;
                const h00 = 2 * t3 - 3 * t2 + 1;
                const h10 = t3 - 2 * t2 + localT;
                const h01 = -2 * t3 + 3 * t2;
                const h11 = t3 - t2;
                const dt = k1.time - k0.time;
                return h00 * k0.value + h10 * dt * k0.outTangent + h01 * k1.value + h11 * dt * k1.inTangent;
            }
        }
        return keyframes[keyframes.length - 1].value;
    }
    handleWrapMode(t, mode) {
        switch (mode) {
            case 'clamp':
                return Math.max(0, Math.min(1, t));
            case 'loop':
                return t - Math.floor(t);
            case 'pingPong':
                const cycle = Math.floor(t);
                const frac = t - cycle;
                return cycle % 2 === 0 ? frac : 1 - frac;
        }
    }
    evaluateMinMaxGradient(gradient, t, seed) {
        switch (gradient.mode) {
            case 'color':
                return gradient.color;
            case 'gradient':
                return this.evaluateGradient(gradient.gradient, t);
            case 'randomBetweenTwoColors':
                const rand = this.seededRandom(seed);
                return [
                    gradient.colorMin[0] + (gradient.colorMax[0] - gradient.colorMin[0]) * rand,
                    gradient.colorMin[1] + (gradient.colorMax[1] - gradient.colorMin[1]) * rand,
                    gradient.colorMin[2] + (gradient.colorMax[2] - gradient.colorMin[2]) * rand,
                    gradient.colorMin[3] + (gradient.colorMax[3] - gradient.colorMin[3]) * rand,
                ];
            case 'randomBetweenTwoGradients':
                const randG = this.seededRandom(seed);
                const minColor = this.evaluateGradient(gradient.gradientMin, t);
                const maxColor = this.evaluateGradient(gradient.gradientMax, t);
                return [
                    minColor[0] + (maxColor[0] - minColor[0]) * randG,
                    minColor[1] + (maxColor[1] - minColor[1]) * randG,
                    minColor[2] + (maxColor[2] - minColor[2]) * randG,
                    minColor[3] + (maxColor[3] - minColor[3]) * randG,
                ];
        }
    }
    evaluateGradient(gradient, t) {
        // Evaluate color
        let color = [1, 1, 1];
        const colorKeys = gradient.colorKeys;
        if (colorKeys.length > 0) {
            if (t <= colorKeys[0].time) {
                color = colorKeys[0].color;
            }
            else if (t >= colorKeys[colorKeys.length - 1].time) {
                color = colorKeys[colorKeys.length - 1].color;
            }
            else {
                for (let i = 0; i < colorKeys.length - 1; i++) {
                    if (t >= colorKeys[i].time && t <= colorKeys[i + 1].time) {
                        const localT = (t - colorKeys[i].time) / (colorKeys[i + 1].time - colorKeys[i].time);
                        color = [
                            colorKeys[i].color[0] + (colorKeys[i + 1].color[0] - colorKeys[i].color[0]) * localT,
                            colorKeys[i].color[1] + (colorKeys[i + 1].color[1] - colorKeys[i].color[1]) * localT,
                            colorKeys[i].color[2] + (colorKeys[i + 1].color[2] - colorKeys[i].color[2]) * localT,
                        ];
                        break;
                    }
                }
            }
        }
        // Evaluate alpha
        let alpha = 1;
        const alphaKeys = gradient.alphaKeys;
        if (alphaKeys.length > 0) {
            if (t <= alphaKeys[0].time) {
                alpha = alphaKeys[0].alpha;
            }
            else if (t >= alphaKeys[alphaKeys.length - 1].time) {
                alpha = alphaKeys[alphaKeys.length - 1].alpha;
            }
            else {
                for (let i = 0; i < alphaKeys.length - 1; i++) {
                    if (t >= alphaKeys[i].time && t <= alphaKeys[i + 1].time) {
                        const localT = (t - alphaKeys[i].time) / (alphaKeys[i + 1].time - alphaKeys[i].time);
                        alpha = alphaKeys[i].alpha + (alphaKeys[i + 1].alpha - alphaKeys[i].alpha) * localT;
                        break;
                    }
                }
            }
        }
        return [color[0], color[1], color[2], alpha];
    }
    evaluateMinMaxVector3(vec, t, seed) {
        switch (vec.mode) {
            case 'constant':
                return vec.value;
            case 'randomBetweenTwoConstants':
                const rand = this.seededRandom(seed);
                return [
                    vec.min[0] + (vec.max[0] - vec.min[0]) * rand,
                    vec.min[1] + (vec.max[1] - vec.min[1]) * rand,
                    vec.min[2] + (vec.max[2] - vec.min[2]) * rand,
                ];
        }
    }
    seededRandom(seed) {
        const x = Math.sin(seed * 12.9898 + 78.233) * 43758.5453;
        return x - Math.floor(x);
    }
    normalizedTime() {
        return this.state.time / this.definition.main.duration;
    }
    // ─── Public API ────────────────────────────────────────
    get particleCount() {
        return this.state.activeCount;
    }
    get isPlaying() {
        return this.state.isPlaying;
    }
    get isPaused() {
        return this.state.isPaused;
    }
    get isStopped() {
        return this.state.isStopped;
    }
    get time() {
        return this.state.time;
    }
    get duration() {
        return this.definition.main.duration;
    }
    getParticles() {
        return this.state.particles.filter((p) => p.remainingLifetime >= 0);
    }
    getDefinition() {
        return this.definition;
    }
    setDefinition(def) {
        this.definition = def;
        this.state = this.createInitialState();
    }
}
// ============================================================
// Factory Functions
// ============================================================
function createDefaultParticleSystem(name) {
    return {
        id: `ps_${Date.now()}`,
        name,
        main: {
            duration: 5,
            looping: true,
            prewarm: false,
            startDelay: { mode: 'constant', value: 0 },
            startLifetime: { mode: 'constant', value: 5 },
            startSpeed: { mode: 'constant', value: 5 },
            startSize3D: false,
            startSize: { mode: 'constant', value: 1 },
            startRotation3D: false,
            startRotation: { mode: 'constant', value: 0 },
            flipRotation: 0,
            startColor: { mode: 'color', color: [1, 1, 1, 1] },
            gravityModifier: { mode: 'constant', value: 0 },
            simulationSpace: 'local',
            simulationSpeed: 1,
            deltaTime: 'scaled',
            scalingMode: 'local',
            playOnAwake: true,
            emitterVelocityMode: 'transform',
            maxParticles: 1000,
            stopAction: 'none',
            cullingMode: 'automatic',
            ringBufferMode: 'disabled',
            ringBufferLoopRange: [0, 1],
        },
        emission: {
            enabled: true,
            rateOverTime: { mode: 'constant', value: 10 },
            rateOverDistance: { mode: 'constant', value: 0 },
            bursts: [],
        },
        shape: {
            type: 'cone',
            angle: 25,
            radius: 1,
            radiusThickness: 1,
            arc: 360,
            arcMode: 'random',
            arcSpeed: 1,
            length: 5,
            emitFrom: 'base',
            position: [0, 0, 0],
            rotation: [0, 0, 0],
            scale: [1, 1, 1],
        },
        velocityOverLifetime: {
            enabled: false,
            space: 'local',
            linear: { mode: 'constant', value: [0, 0, 0] },
            orbital: { mode: 'constant', value: [0, 0, 0] },
            offset: { mode: 'constant', value: [0, 0, 0] },
            radial: { mode: 'constant', value: 0 },
            speedModifier: { mode: 'constant', value: 1 },
        },
        limitVelocityOverLifetime: {
            enabled: false,
            separateAxes: false,
            speed: { mode: 'constant', value: 1 },
            space: 'local',
            dampen: 0.1,
            drag: { mode: 'constant', value: 0 },
            multiplyDragBySize: false,
            multiplyDragByVelocity: false,
        },
        inheritVelocity: {
            enabled: false,
            mode: 'initial',
            curve: { mode: 'constant', value: 0 },
        },
        lifetimeByEmitterSpeed: {
            enabled: false,
            curve: { mode: 'constant', value: 1 },
            speedRange: [0, 1],
        },
        forceOverLifetime: {
            enabled: false,
            space: 'local',
            x: { mode: 'constant', value: 0 },
            y: { mode: 'constant', value: 0 },
            z: { mode: 'constant', value: 0 },
            randomize: false,
        },
        colorOverLifetime: {
            enabled: false,
            color: { mode: 'color', color: [1, 1, 1, 1] },
        },
        colorBySpeed: {
            enabled: false,
            color: { mode: 'color', color: [1, 1, 1, 1] },
            speedRange: [0, 1],
        },
        sizeOverLifetime: {
            enabled: false,
            separateAxes: false,
            size: { mode: 'constant', value: 1 },
        },
        sizeBySpeed: {
            enabled: false,
            separateAxes: false,
            size: { mode: 'constant', value: 1 },
            speedRange: [0, 1],
        },
        rotationOverLifetime: {
            enabled: false,
            separateAxes: false,
            angularVelocity: { mode: 'constant', value: 0 },
        },
        rotationBySpeed: {
            enabled: false,
            separateAxes: false,
            angularVelocity: { mode: 'constant', value: 0 },
            speedRange: [0, 1],
        },
        externalForces: {
            enabled: false,
            multiplier: { mode: 'constant', value: 1 },
            influenceFilter: 'layerMask',
            influenceMask: 0xffffffff,
            forceFields: [],
        },
        noise: {
            enabled: false,
            separateAxes: false,
            strength: { mode: 'constant', value: 1 },
            frequency: 0.5,
            scrollSpeed: 0,
            damping: true,
            octaves: 1,
            octaveMultiplier: 0.5,
            octaveScale: 2,
            quality: 'medium',
            remapEnabled: false,
            remapCurve: { keyframes: [], preWrapMode: 'clamp', postWrapMode: 'clamp' },
            positionAmount: { mode: 'constant', value: 1 },
            rotationAmount: { mode: 'constant', value: 0 },
            sizeAmount: { mode: 'constant', value: 0 },
        },
        collision: {
            enabled: false,
            type: 'planes',
            planes: [{ normal: [0, 1, 0], position: [0, 0, 0] }],
            bounce: { mode: 'constant', value: 0.5 },
            lifetimeLoss: { mode: 'constant', value: 0 },
            minKillSpeed: 0,
            radiusScale: 1,
            dampen: { mode: 'constant', value: 0 },
            enableDynamicColliders: false,
            colliderForce: 0,
            multiplyByCollisionAngle: true,
            multiplyByParticleSpeed: true,
            multiplyByParticleSize: false,
            sendCollisionMessages: false,
        },
        triggers: {
            enabled: false,
            inside: 'ignore',
            outside: 'ignore',
            enter: 'ignore',
            exit: 'ignore',
            colliderQueryMode: 'disabled',
            radiusScale: 1,
        },
        subEmitters: [],
        textureSheetAnimation: {
            enabled: false,
            mode: 'grid',
            tilesX: 1,
            tilesY: 1,
            animation: 'wholeSheet',
            rowIndex: 0,
            rowMode: 'custom',
            timeMode: 'lifetime',
            frameOverTime: { mode: 'constant', value: 0 },
            startFrame: { mode: 'constant', value: 0 },
            cycleCount: 1,
            speedRange: [0, 1],
            fps: 30,
            sprites: [],
        },
        lights: {
            enabled: false,
            ratio: 1,
            randomDistribution: true,
            light: {
                type: 'point',
                color: { mode: 'color', color: [1, 1, 1, 1] },
                intensity: { mode: 'constant', value: 1 },
                range: { mode: 'constant', value: 1 },
            },
            useParticleColor: true,
            sizeAffectsRange: true,
            alphaAffectsIntensity: true,
            maxLights: 20,
        },
        trails: {
            enabled: false,
            ratio: 1,
            lifetime: { mode: 'constant', value: 1 },
            minVertexDistance: 0.2,
            textureMode: 'stretch',
            worldSpace: false,
            dieWithParticle: true,
            sizeAffectsWidth: true,
            sizeAffectsLifetime: false,
            inheritParticleColor: true,
            colorOverLifetime: { mode: 'color', color: [1, 1, 1, 1] },
            widthOverTrail: { mode: 'constant', value: 1 },
            colorOverTrail: { mode: 'color', color: [1, 1, 1, 1] },
            generateLightingData: false,
            shadowBias: 0.5,
        },
        customData: {
            enabled: false,
            stream1: { mode: 'disabled', components: 4 },
            stream2: { mode: 'disabled', components: 4 },
        },
        renderer: {
            enabled: true,
            renderMode: 'billboard',
            material: '',
            sortMode: 'none',
            sortingFudge: 0,
            minParticleSize: 0,
            maxParticleSize: 0.5,
            cameraVelocityScale: 0,
            velocityScale: 0,
            lengthScale: 2,
            normalDirection: 1,
            shadowCastingMode: 'off',
            receiveShadows: false,
            motionVectors: 'cameraMotionOnly',
            lightProbes: 'off',
            reflectionProbes: 'off',
            maskInteraction: 'none',
            pivot: [0, 0, 0],
            flip: [0, 0, 0],
            allowRoll: true,
            freeformStretching: false,
            rotateWithStretchDirection: true,
            enableGPUInstancing: false,
        },
    };
}
function createParticleSystem(def) {
    return new ParticleSystem2(def);
}
// ============================================================
// Preset Systems
// ============================================================
function createFireParticleSystem() {
    const def = createDefaultParticleSystem('Fire');
    def.main.startLifetime = { mode: 'randomBetweenTwoConstants', min: 0.5, max: 1.5 };
    def.main.startSize = { mode: 'randomBetweenTwoConstants', min: 0.1, max: 0.5 };
    def.main.startColor = {
        mode: 'randomBetweenTwoColors',
        colorMin: [1, 0.5, 0, 1],
        colorMax: [1, 0.2, 0, 1],
    };
    def.main.gravityModifier = { mode: 'constant', value: -0.5 };
    def.emission.rateOverTime = { mode: 'constant', value: 50 };
    def.shape = {
        type: 'cone',
        angle: 15,
        radius: 0.2,
        radiusThickness: 1,
        arc: 360,
        arcMode: 'random',
        arcSpeed: 1,
        length: 1,
        emitFrom: 'base',
        position: [0, 0, 0],
        rotation: [0, 0, 0],
        scale: [1, 1, 1],
    };
    def.colorOverLifetime = {
        enabled: true,
        color: {
            mode: 'gradient',
            gradient: {
                colorKeys: [
                    { time: 0, color: [1, 0.8, 0] },
                    { time: 0.3, color: [1, 0.3, 0] },
                    { time: 1, color: [0.2, 0, 0] },
                ],
                alphaKeys: [
                    { time: 0, alpha: 1 },
                    { time: 0.7, alpha: 0.8 },
                    { time: 1, alpha: 0 },
                ],
                mode: 'blend',
            },
        },
    };
    def.sizeOverLifetime = {
        enabled: true,
        separateAxes: false,
        size: {
            mode: 'curve',
            curve: {
                keyframes: [
                    { time: 0, value: 0.5, inTangent: 0, outTangent: 1, mode: 'auto' },
                    { time: 0.3, value: 1, inTangent: 0, outTangent: 0, mode: 'auto' },
                    { time: 1, value: 0, inTangent: -1, outTangent: 0, mode: 'auto' },
                ],
                preWrapMode: 'clamp',
                postWrapMode: 'clamp',
            },
            multiplier: 1,
        },
    };
    def.noise = {
        enabled: true,
        separateAxes: false,
        strength: { mode: 'constant', value: 0.3 },
        frequency: 1,
        scrollSpeed: 1,
        damping: true,
        octaves: 2,
        octaveMultiplier: 0.5,
        octaveScale: 2,
        quality: 'medium',
        remapEnabled: false,
        remapCurve: { keyframes: [], preWrapMode: 'clamp', postWrapMode: 'clamp' },
        positionAmount: { mode: 'constant', value: 1 },
        rotationAmount: { mode: 'constant', value: 0 },
        sizeAmount: { mode: 'constant', value: 0 },
    };
    def.renderer.renderMode = 'billboard';
    return def;
}
function createSmokeParticleSystem() {
    const def = createDefaultParticleSystem('Smoke');
    def.main.startLifetime = { mode: 'randomBetweenTwoConstants', min: 3, max: 5 };
    def.main.startSize = { mode: 'randomBetweenTwoConstants', min: 0.5, max: 1 };
    def.main.startColor = {
        mode: 'randomBetweenTwoColors',
        colorMin: [0.5, 0.5, 0.5, 0.5],
        colorMax: [0.3, 0.3, 0.3, 0.3],
    };
    def.main.gravityModifier = { mode: 'constant', value: -0.1 };
    def.emission.rateOverTime = { mode: 'constant', value: 20 };
    def.shape = {
        type: 'circle',
        radius: 0.3,
        radiusThickness: 1,
        arc: 360,
        arcMode: 'random',
        arcSpeed: 1,
        position: [0, 0, 0],
        rotation: [0, 0, 0],
        scale: [1, 1, 1],
    };
    def.sizeOverLifetime = {
        enabled: true,
        separateAxes: false,
        size: {
            mode: 'curve',
            curve: {
                keyframes: [
                    { time: 0, value: 0.3, inTangent: 0, outTangent: 2, mode: 'auto' },
                    { time: 1, value: 3, inTangent: 0.5, outTangent: 0, mode: 'auto' },
                ],
                preWrapMode: 'clamp',
                postWrapMode: 'clamp',
            },
            multiplier: 1,
        },
    };
    def.colorOverLifetime = {
        enabled: true,
        color: {
            mode: 'gradient',
            gradient: {
                colorKeys: [
                    { time: 0, color: [0.4, 0.4, 0.4] },
                    { time: 1, color: [0.6, 0.6, 0.6] },
                ],
                alphaKeys: [
                    { time: 0, alpha: 0 },
                    { time: 0.1, alpha: 0.5 },
                    { time: 0.8, alpha: 0.3 },
                    { time: 1, alpha: 0 },
                ],
                mode: 'blend',
            },
        },
    };
    def.rotationOverLifetime = {
        enabled: true,
        separateAxes: false,
        angularVelocity: { mode: 'randomBetweenTwoConstants', min: -0.5, max: 0.5 },
    };
    def.noise = {
        enabled: true,
        separateAxes: false,
        strength: { mode: 'constant', value: 0.5 },
        frequency: 0.3,
        scrollSpeed: 0.2,
        damping: true,
        octaves: 2,
        octaveMultiplier: 0.5,
        octaveScale: 2,
        quality: 'medium',
        remapEnabled: false,
        remapCurve: { keyframes: [], preWrapMode: 'clamp', postWrapMode: 'clamp' },
        positionAmount: { mode: 'constant', value: 1 },
        rotationAmount: { mode: 'constant', value: 0 },
        sizeAmount: { mode: 'constant', value: 0 },
    };
    return def;
}
function createSparkParticleSystem() {
    const def = createDefaultParticleSystem('Sparks');
    def.main.startLifetime = { mode: 'randomBetweenTwoConstants', min: 0.2, max: 0.5 };
    def.main.startSize = { mode: 'randomBetweenTwoConstants', min: 0.02, max: 0.05 };
    def.main.startSpeed = { mode: 'randomBetweenTwoConstants', min: 3, max: 8 };
    def.main.startColor = {
        mode: 'randomBetweenTwoColors',
        colorMin: [1, 0.8, 0, 1],
        colorMax: [1, 0.5, 0, 1],
    };
    def.main.gravityModifier = { mode: 'constant', value: 1 };
    def.emission.rateOverTime = { mode: 'constant', value: 0 };
    def.emission.bursts = [
        {
            time: 0,
            count: { mode: 'randomBetweenTwoConstants', min: 10, max: 30 },
            cycles: 1,
            interval: 0.01,
            probability: 1,
        },
    ];
    def.shape = {
        type: 'sphere',
        radius: 0.1,
        radiusThickness: 0,
        arc: 360,
        arcMode: 'random',
        arcSpeed: 1,
        position: [0, 0, 0],
        rotation: [0, 0, 0],
        scale: [1, 1, 1],
    };
    def.colorOverLifetime = {
        enabled: true,
        color: {
            mode: 'gradient',
            gradient: {
                colorKeys: [
                    { time: 0, color: [1, 0.9, 0.5] },
                    { time: 0.5, color: [1, 0.5, 0] },
                    { time: 1, color: [0.5, 0, 0] },
                ],
                alphaKeys: [
                    { time: 0, alpha: 1 },
                    { time: 0.8, alpha: 1 },
                    { time: 1, alpha: 0 },
                ],
                mode: 'blend',
            },
        },
    };
    def.trails = {
        enabled: true,
        ratio: 1,
        lifetime: { mode: 'constant', value: 0.1 },
        minVertexDistance: 0.01,
        textureMode: 'stretch',
        worldSpace: true,
        dieWithParticle: true,
        sizeAffectsWidth: true,
        sizeAffectsLifetime: false,
        inheritParticleColor: true,
        colorOverLifetime: { mode: 'color', color: [1, 1, 1, 1] },
        widthOverTrail: {
            mode: 'curve',
            curve: {
                keyframes: [
                    { time: 0, value: 1, inTangent: 0, outTangent: 0, mode: 'auto' },
                    { time: 1, value: 0, inTangent: -1, outTangent: 0, mode: 'auto' },
                ],
                preWrapMode: 'clamp',
                postWrapMode: 'clamp',
            },
            multiplier: 1,
        },
        colorOverTrail: {
            mode: 'gradient',
            gradient: {
                colorKeys: [
                    { time: 0, color: [1, 0.8, 0] },
                    { time: 1, color: [1, 0.2, 0] },
                ],
                alphaKeys: [
                    { time: 0, alpha: 1 },
                    { time: 1, alpha: 0 },
                ],
                mode: 'blend',
            },
        },
        generateLightingData: false,
        shadowBias: 0.5,
    };
    def.collision = {
        enabled: true,
        type: 'planes',
        planes: [{ normal: [0, 1, 0], position: [0, 0, 0] }],
        bounce: { mode: 'randomBetweenTwoConstants', min: 0.2, max: 0.6 },
        lifetimeLoss: { mode: 'constant', value: 0.2 },
        minKillSpeed: 0.1,
        radiusScale: 1,
        dampen: { mode: 'constant', value: 0.3 },
        enableDynamicColliders: false,
        colliderForce: 0,
        multiplyByCollisionAngle: true,
        multiplyByParticleSpeed: true,
        multiplyByParticleSize: false,
        sendCollisionMessages: false,
    };
    def.renderer.renderMode = 'stretchedBillboard';
    def.renderer.velocityScale = 0.1;
    def.renderer.lengthScale = 5;
    return def;
}

exports.ParticleSystem2 = ParticleSystem2;
exports.createDefaultParticleSystem = createDefaultParticleSystem;
exports.createFireParticleSystem = createFireParticleSystem;
exports.createParticleSystem = createParticleSystem;
exports.createSmokeParticleSystem = createSmokeParticleSystem;
exports.createSparkParticleSystem = createSparkParticleSystem;
//# sourceMappingURL=ParticleSystem2.js.map
