import { useRef, useEffect, useMemo, useCallback } from 'react'; import { ParticleSystem, ParticleSystemParameters, RenderMode, ConstantValue, ConstantColor, SphereEmitter, Vector4, } from 'three.quarks'; import { MeshBasicMaterial, AdditiveBlending, DoubleSide } from 'three'; import { useQuarks } from '../QuarksContext'; import { toValueGenerator, toColorGenerator, isDefined } from '../utils'; import type { ParticleSystemProps, UseParticleSystemReturn } from '../types'; /** * Convert component props to ParticleSystemParameters */ function propsToParameters(props: ParticleSystemProps): ParticleSystemParameters { const { // Core simulation autoDestroy, looping, prewarm, duration, // Emission shape, emissionOverTime, emissionOverDistance, emissionBursts, // Particle initial properties startLife, startSpeed, startRotation, startSize, startColor, startTileIndex, startLength, // Behaviors behaviors, // Rendering instancingGeometry, renderMode, rendererEmitterSettings, speedFactor, material, layers, // Tiling uTileCount, vTileCount, blendTiles, // Soft particles softParticles, softNearFade, softFarFade, // Ordering renderOrder, // Transform worldSpace, onlyUsedByOther, } = props; // Create default material if not provided const finalMaterial = material ?? new MeshBasicMaterial({ color: 0xffffff, transparent: true, blending: AdditiveBlending, side: DoubleSide, depthWrite: false, }); const params: ParticleSystemParameters = { material: finalMaterial, }; // Core simulation if (isDefined(autoDestroy)) params.autoDestroy = autoDestroy; if (isDefined(looping)) params.looping = looping; if (isDefined(prewarm)) params.prewarm = prewarm; if (isDefined(duration)) params.duration = duration; // Emission if (isDefined(shape)) params.shape = shape; if (isDefined(emissionOverTime)) params.emissionOverTime = toValueGenerator(emissionOverTime); if (isDefined(emissionOverDistance)) params.emissionOverDistance = toValueGenerator(emissionOverDistance); if (isDefined(emissionBursts)) params.emissionBursts = emissionBursts; // Particle initial properties if (isDefined(startLife)) params.startLife = toValueGenerator(startLife); if (isDefined(startSpeed)) params.startSpeed = toValueGenerator(startSpeed); if (isDefined(startRotation)) { // Handle both FlexibleValue and RotationGenerator if (typeof startRotation === 'number' || Array.isArray(startRotation)) { params.startRotation = toValueGenerator(startRotation); } else { params.startRotation = startRotation as any; } } if (isDefined(startSize)) { // Handle both FlexibleValue and Vector3Generator if (typeof startSize === 'number' || Array.isArray(startSize)) { params.startSize = toValueGenerator(startSize); } else { params.startSize = startSize as any; } } if (isDefined(startColor)) params.startColor = toColorGenerator(startColor); if (isDefined(startTileIndex)) params.startTileIndex = startTileIndex; // startLength is handled via rendererEmitterSettings for Trail mode // Behaviors if (isDefined(behaviors)) params.behaviors = behaviors; // Rendering if (isDefined(instancingGeometry)) params.instancingGeometry = instancingGeometry; if (isDefined(renderMode)) params.renderMode = renderMode; if (isDefined(rendererEmitterSettings)) params.rendererEmitterSettings = rendererEmitterSettings; if (isDefined(speedFactor)) params.speedFactor = speedFactor; if (isDefined(layers)) params.layers = layers; // Tiling if (isDefined(uTileCount)) params.uTileCount = uTileCount; if (isDefined(vTileCount)) params.vTileCount = vTileCount; if (isDefined(blendTiles)) params.blendTiles = blendTiles; // Soft particles if (isDefined(softParticles)) params.softParticles = softParticles; if (isDefined(softNearFade)) params.softNearFade = softNearFade; if (isDefined(softFarFade)) params.softFarFade = softFarFade; // Ordering if (isDefined(renderOrder)) params.renderOrder = renderOrder; // Transform if (isDefined(worldSpace)) params.worldSpace = worldSpace; if (isDefined(onlyUsedByOther)) params.onlyUsedByOther = onlyUsedByOther; return params; } /** * Create a stable dependency key from props for memoization */ function getConfigKey(props: ParticleSystemProps): string { // Include all configuration props that would require recreating the system return JSON.stringify({ autoDestroy: props.autoDestroy, looping: props.looping, prewarm: props.prewarm, duration: props.duration, renderMode: props.renderMode, uTileCount: props.uTileCount, vTileCount: props.vTileCount, blendTiles: props.blendTiles, softParticles: props.softParticles, worldSpace: props.worldSpace, onlyUsedByOther: props.onlyUsedByOther, // Note: shape, behaviors, materials are reference-based // Users should memoize these themselves }); } /** * Hook for creating and managing a particle system imperatively. * * Auto-registers with BatchedRenderer and cleans up on unmount. * * @param props - ParticleSystemProps configuration * @returns Object with system, emitter, and control functions * * @example * ```tsx * function MyParticles() { * const { system, emitter, play, pause, restart } = useParticleSystem({ * duration: 5, * looping: true, * startLife: [1, 2], * startSpeed: 5, * emissionOverTime: 100, * }) * * return * } * ``` */ export function useParticleSystem(props: ParticleSystemProps): UseParticleSystemReturn { const { batchedRenderer } = useQuarks(); const systemRef = useRef(null); const configKey = getConfigKey(props); // Create system with memoization based on config const system = useMemo(() => { // Clean up old system if exists if (systemRef.current) { batchedRenderer.deleteSystem(systemRef.current); } const params = propsToParameters(props); const newSystem = new ParticleSystem(params); batchedRenderer.addSystem(newSystem); systemRef.current = newSystem; // Handle autoPlay (default true) if (props.autoPlay === false) { newSystem.pause(); } return newSystem; // eslint-disable-next-line react-hooks/exhaustive-deps }, [batchedRenderer, configKey]); // Cleanup on unmount useEffect(() => { return () => { if (systemRef.current) { batchedRenderer.deleteSystem(systemRef.current); systemRef.current = null; } }; }, [batchedRenderer]); // Control functions const play = useCallback(() => { systemRef.current?.play(); }, []); const pause = useCallback(() => { systemRef.current?.pause(); }, []); const restart = useCallback(() => { systemRef.current?.restart(); }, []); const stop = useCallback(() => { systemRef.current?.stop(); }, []); return { system, emitter: system.emitter, play, pause, restart, stop, }; }