import { BufferGeometry , Float32BufferAttribute , Points , Color , Vector2 , ShaderMaterial , MathUtils , BufferAttribute } from 'three' ; import { Tween , Easing } from '@tweenjs/tween.js' ; import { ParticleShader } from './shaders/particle-shader' ; import { ShaderUtils } from './shaders/shader-utils' ; import { LoopableTransitionConfig } from '../transition' ; interface ParticleMoveOffset { distance : number ; angle : number ; } interface ParticleSwayOffset { x : number ; y : number ; } type ParticleGroupConfigs = { [ name ] : ParticleGroupConfig ; } ; interface ParticleGroupConfig { name : string ; amount : number ; minSize ? : number ; maxSize ? : number ; minGradient ? : number ; maxGradient ? : number ; minOpacity ? : number ; maxOpacity ? : number ; color ? : number ; smoothing ? : number ; } interface ParticleTween { offsetX : number ; offsetY : number ; } type ParticleGroups = { [ name ] : Required < ParticleGroup > ; } ; interface ParticleGroup extends ParticleGroupConfig { index : number ; swayOffset : Vector2 ; positionTransition : Tween < ParticleTween > ; swayTransition : Tween < ParticleTween > ; } class Particles { private _width : number ; private _height : number ; private _maxDepth : number ; private _groups : ParticleGroups = { } ; private _particles : Points ; private _positions : number [ ] = [ ] ; param { number } width param { number } height param { number } maxDepth constructor ( width , height , maxDepth ) { this . _width = width ; this . _height = height ; this . _maxDepth = maxDepth ; const geometry = new BufferGeometry ( ) ; geometry . setAttribute ( 'position' , new Float32BufferAttribute ( 0 , 3 ) ) ; geometry . setAttribute ( 'size' , new Float32BufferAttribute ( 0 , 1 ) ) ; geometry . setAttribute ( 'gradient' , new Float32BufferAttribute ( 0 , 1 ) ) ; geometry . setAttribute ( 'opacity' , new Float32BufferAttribute ( 0 , 1 ) ) ; geometry . setAttribute ( 'color' , new Float32BufferAttribute ( 0 , 3 ) ) ; this . _particles = new Points ( geometry , ShaderUtils . createShaderMaterial ( ParticleShader ) ) ; } returns getConfigs ( ) { const configs = { } ; for ( const group of Object . values ( this . _groups ) ) { const { name , amount , minSize , maxSize , minGradient , maxGradient , minOpacity , maxOpacity , color } = group ; configs [ name ] = { name , amount , minSize , maxSize , minGradient , maxGradient , minOpacity , maxOpacity , color } ; } return configs ; } param { string } name returns isMoving ( name ) { return this . _groups [ name ] ?. positionTransition . isPlaying ( ) ?? false ; } param { string } name returns isSwaying ( name ) { return this . _groups [ name ] ?. swayTransition . isPlaying ( ) ?? false ; } param { ParticleGroupConfig | ParticleGroupConfig [ ] } config generate ( configs ) { this . removeAll ( ) ; configs = Array . isArray ( configs ) ? configs : [ configs ] ; let index = 0 ; for ( const config of configs ) { const { name , amount = 0 , minSize = 0 , maxSize = 0 , minGradient = 0 , maxGradient = 1 , minOpacity = 0 , maxOpacity = 1 , color = 0xffffff , smoothing = 0.5 , } = config ; for ( let i = 0 ; i < amount || 0 ; ++ i ) { const x = ( - this . _width / 2 ) + Math . random ( ) * this . _width ; const y = ( - this . _height / 2 ) + Math . random ( ) * this . _height ; const z = ( this . _maxDepth / 4 ) * Math . random ( ) ; this . _positions . push ( x , y , z ) ; } this . _groups [ name ] = { name , index , amount , minSize , maxSize , minGradient , maxGradient , minOpacity , maxOpacity , color , smoothing , swayOffset : new Vector2 ( 0 , 0 ) , positionTransition : new Tween ( { offsetX : 0 , offsetY : 0 } ) , swayTransition : new Tween ( { offsetX : 0 , offsetY : 0 } ) , } ; index += amount ; } const geometry = new BufferGeometry ( ) ; geometry . setAttribute ( 'position' , new Float32BufferAttribute ( index * 3 , 3 ) ) ; geometry . setAttribute ( 'color' , new Float32BufferAttribute ( index * 3 , 3 ) ) ; geometry . setAttribute ( 'size' , new Float32BufferAttribute ( index , 1 ) ) ; geometry . setAttribute ( 'gradient' , new Float32BufferAttribute ( index , 1 ) ) ; geometry . setAttribute ( 'opacity' , new Float32BufferAttribute ( index , 1 ) ) ; const material = ShaderUtils . createShaderMaterial ( ParticleShader ) ; material . transparent = true ; this . _particles . geometry = geometry ; this . _particles . material = material ; } /**
     * Removes all particle groups.
     */ removeAll ( ) { for ( const group in this . _groups ) { this . sway ( group , false ) ; this . move ( group , false ) ; } this . _positions = [ ] ; this . _groups = { } ; this . _particles . geometry . dispose ( ) ; ( this . _particles . material as ShaderMaterial ) . dispose ( ) ; } param { Vector2 } position param { Vector2 } offset returns private _getNewPosition ( position , offset ) { let { x : offsetX , y : offsetY } = offset ; offsetX %= this . _width ; offsetY %= this . _height ; let x = position . x + offsetX ; let y = position . y + offsetY ; const halfWidth = this . _width / 2 ; const halfHeight = this . _height / 2 ; if ( Math . abs ( position . x + offsetX ) > halfWidth ) { x = offsetX > 0 ? - halfWidth + ( ( ( position . x + offsetX ) - halfWidth ) % this . _width ) : halfWidth - ( ( Math . abs ( position . x + offsetX ) - halfWidth ) % this . _width ) ; } if ( Math . abs ( position . y + offsetY ) > halfHeight ) { y = offsetY > 0 ? - halfHeight + ( ( ( position . y + offsetY ) - halfHeight ) % this . _height ) : halfHeight - ( ( Math . abs ( position . y + offsetY ) - halfHeight ) % this . _height ) ; } return new Vector2 ( x , y ) ; } param { number } index param { number } amount param { number [ ] } positions param { Vector2 } offset private _updatePositions ( index , amount , positions , offset ) { for ( let i = index ; i < index + amount ; ++ i ) { const { x , y } = this . _getNewPosition ( new Vector2 ( positions [ i * 3 ] , positions [ i * 3 + 1 ] ) , offset ) ; this . _positions [ i * 3 ] = x ; this . _positions [ i * 3 + 1 ] = y ; } } param { string } name param { ParticleMoveOffset | boolean } offset param { LoopableTransitionConfig } transition move ( name , offset , transition = { } ) { const group = this . _groups [ name ] ; const { index , amount } = group ; if ( typeof offset === 'boolean' ) { if ( ! offset ) { group . positionTransition . stop ( ) ; } return ; } group . positionTransition . stop ( ) ; const { loop = false , duration = 0 , easing = Easing . Linear . None , onStart = ( ) => ( { } ) , onUpdate = ( ) => ( { } ) , onComplete = ( ) => ( { } ) , onStop = ( ) => ( { } ) , } = transition ; const { distance , angle } = offset ; const offsetX = distance * Math . cos ( MathUtils . degToRad ( angle ) ) ; const offsetY = distance * Math . sin ( MathUtils . degToRad ( angle ) ) ; if ( duration > 0 ) { const startPositions = this . _positions . slice ( ) ; group . positionTransition = new Tween ( { offsetX : 0 , offsetY : 0 } ) . to ( { offsetX , offsetY } , duration * 1000 ) . easing ( easing ) . onStart ( onStart ) . onUpdate ( ( { offsetX , offsetY } ) => { this . _updatePositions ( index , amount , startPositions , new Vector2 ( offsetX , offsetY ) ) ; onUpdate ( ) ; } ) . onComplete ( ( ) => { if ( loop ) { this . move ( name , offset , transition ) ; } onComplete ( ) ; } ) . onStop ( onStop ) . start ( ) ; } else { this . _updatePositions ( index , amount , this . _positions , new Vector2 ( offsetX , offsetY ) ) ; } } param { string } name param { ParticleSwayOffset | boolean } offset param { LoopableTransitionConfig } transition sway ( name , offset , transition = { } ) { const group = this . _groups [ name ] ; const { swayOffset } = group ; if ( typeof offset === 'boolean' ) { if ( ! offset ) { group . swayTransition . stop ( ) ; } return ; } group . swayTransition . stop ( ) ; const { loop = false , duration = 0 , easing = Easing . Linear . None , onStart = ( ) => ( { } ) , onUpdate = ( ) => ( { } ) , onComplete = ( ) => ( { } ) , onStop = ( ) => ( { } ) , } = transition ; const { x , y } = offset ; group . swayTransition = new Tween ( { offsetX : swayOffset . x , offsetY : swayOffset . y , } ) . to ( { offsetX : - x + Math . random ( ) * x * 2 , offsetY : - y + Math . random ( ) * y * 2 , } , duration * 1000 ) . easing ( easing ) . onStart ( onStart ) . onUpdate ( ( { offsetX , offsetY } ) => { swayOffset . set ( offsetX , offsetY ) ; onUpdate ( ) ; } ) . onComplete ( ( ) => { if ( loop ) { this . sway ( name , offset , transition ) ; } onComplete ( ) ; } ) . onStop ( onStop ) . start ( ) ; } param { number } prevValue param { number } minValue param { number } maxValue param { number } smoothing returns private _generateNewRandomAveragedValue ( prevValue , minValue , maxValue , smoothing = 0.5 ) { smoothing = Math . min ( smoothing , 0.95 ) ; const offset = ( maxValue - minValue ) / 2 ; const nextValue = Math . max ( Math . min ( prevValue + ( - offset + Math . random ( ) * offset * 2 ) , maxValue ) , minValue ) ; const smoothedValue = ( prevValue * smoothing ) + ( nextValue * ( 1 - smoothing ) ) ; return Math . max ( Math . min ( smoothedValue , maxValue ) , minValue ) ; } /**
     * Updates the positions of the particles. Should be called on every render frame.
     */ update ( ) { const { attributes } = this . _particles . geometry ; const { position : positions , size : sizes , gradient : gradients , opacity : opacities , color : colors , } = attributes ; for ( const group of Object . values ( this . _groups ) ) { const { index , amount , minSize , maxSize , minGradient , maxGradient , minOpacity , maxOpacity , color , smoothing , swayOffset , } = group ; for ( let i = index ; i < index + amount ; ++ i ) { const position = this . _getNewPosition ( new Vector2 ( this . _positions [ i * 3 ] , this . _positions [ i * 3 + 1 ] ) , swayOffset ) ; const rgb = new Color ( color ) ; positions . setXYZ ( i , position . x , position . y , this . _positions [ i * 3 + 2 ] ) ; colors . setXYZ ( i , rgb . r , rgb . g , rgb . b ) ; sizes . setX ( i , this . _generateNewRandomAveragedValue ( sizes . getX ( i ) , minSize , maxSize , smoothing ) ) ; gradients . setX ( i , this . _generateNewRandomAveragedValue ( gradients . getX ( i ) , minGradient , maxGradient , smoothing ) ) ; opacities . setX ( i , this . _generateNewRandomAveragedValue ( opacities . getX ( i ) , minOpacity , maxOpacity , smoothing ) ) ; } } ( attributes . position as BufferAttribute ) . needsUpdate = true ; ( attributes . size as BufferAttribute ) . needsUpdate = true ; ( attributes . gradient as BufferAttribute ) . needsUpdate = true ; ( attributes . opacity as BufferAttribute ) . needsUpdate = true ; ( attributes . color as BufferAttribute ) . needsUpdate = true ; } returns get object ( ) : Points { return this . _particles ; } /**
     * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
     */ dispose ( ) { this . removeAll ( ) ; this . _particles . geometry . dispose ( ) ; ( this . _particles . material as ShaderMaterial ) . dispose ( ) ; } } export { ParticleMoveOffset , ParticleSwayOffset , ParticleGroupConfigs , ParticleGroupConfig , Particles , } ; export default Particles ;