import { orientationSupport } from '@src/common/constants' ; import Shape from '@src/common/shape' ; import { IElement , Options } from '@src/types/particle' ; import { ValueOf } from '@src/types/utility-types' ; import { calcQuantity , isElement , isNull , offset , pInt , randomInRange , randomSpeed , } from '@src/utils' ; export default class Particle extends Shape < Options > { static defaultConfig : Options = { num : 0.12 , maxR : 2.4 , minR : 0.6 , maxSpeed : 1 , minSpeed : 0.1 , proximity : 0.2 , range : 0.2 , lineWidth : 0.2 , lineShape : 'spider' , eventElem : null , parallax : false , parallaxLayer : [ 1 , 2 , 3 ] , parallaxStrength : 3 , } ; protected elements ! : IElement [ ] ; private positionX ? : number ; private positionY ? : number ; private mouseX = 0 ; private mouseY = 0 ; private lineShapeMaker ? : ( x : number , y : number , sx : number , sy : number , cb : ( ) => void ) => void ; constructor ( selector , options ? ) { super ( Particle . defaultConfig , selector , options ) ; this . bootstrap ( ) ; } /**
     * 初始化数据和运行程序
     */ protected init ( ) { this . ownResizeEvent ( ) ; this . optionsNormalize ( ) ; if ( this . options . range > 0 ) { this . positionX = Math . random ( ) * this . canvasWidth ; this . positionY = Math . random ( ) * this . canvasHeight ; this . defineLineShape ( ) ; this . positionEvent ( ) ; } this . mouseX = this . mouseY = 0 ; this . parallaxEvent ( ) ; this . createDots ( ) ; } /**
     * 标准化配置参数，参考 calcQuantity 方法描述。
     * 如:
     *   num: 0.5  =>  表示 0.5 倍画布宽度  =>  标准化为具体数值，如 100
     *   num: 100  =>  表示具体数值  => 标准化结果还是 100
     */ private optionsNormalize ( ) { const { canvasWidth , options } = this ; const props = [ 'num' , 'proximity' , 'range' ] as const ; props . forEach ( ( prop ) => { options [ prop ] = pInt ( calcQuantity ( options [ prop ] , canvasWidth ) ) ; } ) ; if ( ! isElement ( options . eventElem ) && options . eventElem !== document ) { options . eventElem = this . canvas ; } } /**
     * 根据配置参数生成对应形状的连线函数
     */ private defineLineShape ( ) { const { proximity , range , lineShape } = this . options ; switch ( lineShape ) { case 'cube' : this . lineShapeMaker = ( x , y , sx , sy , cb ) => { const { positionX , positionY } = this ; if ( Math . abs ( x - sx ) <= proximity && Math . abs ( y - sy ) <= proximity && Math . abs ( x - positionX ! ) <= range && Math . abs ( y - positionY ! ) <= range && Math . abs ( sx - positionX ! ) <= range && Math . abs ( sy - positionY ! ) <= range ) { cb ( ) ; } } ; break ; default : this . lineShapeMaker = ( x , y , sx , sy , cb ) => { const { positionX , positionY } = this ; if ( Math . abs ( x - sx ) <= proximity && Math . abs ( y - sy ) <= proximity && ( ( Math . abs ( x - positionX ! ) <= range && Math . abs ( y - positionY ! ) <= range ) || ( Math . abs ( sx - positionX ! ) <= range && Math . abs ( sy - positionY ! ) <= range ) ) ) { cb ( ) ; } } ; } } /**
     * 根据配置参数创建许多粒子（纯数据）
     * 最后通过 draw 函数绘制真实可见的图形
     */ private createDots ( ) { const { canvasWidth , canvasHeight , getColor } = this ; const { maxR , minR , maxSpeed , minSpeed , parallaxLayer } = this . options ; const layerLength = parallaxLayer . length ; let { num } = this . options ; while ( num -- ) { const r = randomInRange ( maxR , minR ) ; this . elements . push ( { r , x : randomInRange ( canvasWidth - r , r ) , y : randomInRange ( canvasHeight - r , r ) , vx : randomSpeed ( maxSpeed , minSpeed ) , vy : randomSpeed ( maxSpeed , minSpeed ) , color : getColor ( ) , shape : this . getShapeData ( ) , parallaxLayer : parallaxLayer [ Math . floor ( Math . random ( ) * layerLength ) ] , parallaxOffsetX : 0 , parallaxOffsetY : 0 , } ) ; } } /**
     * 绘制粒子
     */ protected draw ( ) { const { ctx } = this ; const { lineWidth } = this . options ; this . clearCanvasAndSetGlobalAttrs ( ) ; ctx . lineWidth = lineWidth ; this . updateXY ( ) ; this . elements . forEach ( ( dot ) => { const { x , y , parallaxOffsetX , parallaxOffsetY } = dot ; this . drawShape ( { ... dot , x : x + parallaxOffsetX , y : y + parallaxOffsetY , } ) ; } ) ; this . connectDots ( ) ; this . requestAnimationFrame ( ) ; } /**
     * 连接粒子，绘制线段
     */ private connectDots ( ) { if ( this . options . range <= 0 ) return ; const { elements , ctx , lineShapeMaker } = this ; const length = elements . length ; elements . forEach ( ( dot , i ) => { const x = dot . x + dot . parallaxOffsetX ; const y = dot . y + dot . parallaxOffsetY ; while ( ++ i < length ) { const sibDot = elements [ i ] ; const sx = sibDot . x + sibDot . parallaxOffsetX ; const sy = sibDot . y + sibDot . parallaxOffsetY ; lineShapeMaker ?. ( x , y , sx , sy , ( ) => { ctx . save ( ) ; ctx . beginPath ( ) ; ctx . moveTo ( x , y ) ; ctx . lineTo ( sx , sy ) ; ctx . strokeStyle = dot . color ; ctx . stroke ( ) ; ctx . restore ( ) ; } ) ; } } ) ; } /**
     * 更新粒子坐标
     */ private updateXY ( ) { const { isPaused , mouseX , mouseY , canvasWidth , canvasHeight } = this ; const { parallax , parallaxStrength } = this . options ; if ( isPaused ) return ; this . elements . forEach ( ( dot ) => { if ( parallax ) { const divisor = parallaxStrength * dot . parallaxLayer ; dot . parallaxOffsetX += ( mouseX / divisor - dot . parallaxOffsetX ) / 10 ; dot . parallaxOffsetY += ( mouseY / divisor - dot . parallaxOffsetY ) / 10 ; } dot . x += dot . vx ; dot . y += dot . vy ; const { r , parallaxOffsetX , parallaxOffsetY } = dot ; let { x , y } = dot ; x += parallaxOffsetX ; y += parallaxOffsetY ; if ( x + r >= canvasWidth ) { dot . vx = - Math . abs ( dot . vx ) ; } else if ( x - r <= 0 ) { dot . vx = Math . abs ( dot . vx ) ; } if ( y + r >= canvasHeight ) { dot . vy = - Math . abs ( dot . vy ) ; } else if ( y - r <= 0 ) { dot . vy = Math . abs ( dot . vy ) ; } } ) ; } /**
     * 获取绑定的 DOM 元素（eventElem）的 offset 值
     */ private getEventElemOffset ( ) { const { eventElem } = this . options ; return eventElem === document ? null : offset ( eventElem as HTMLElement ) ; } param move param orientation private eventProxy ( move , orientation ) { const { eventElem } = this . options ; let handleOrientation ; if ( orientationSupport ) { handleOrientation = ( e ) => { if ( this . isPaused || isNull ( e . beta ) ) return ; orientation ( Math . min ( Math . max ( e . beta ! , - 90 ) , 90 ) , e . gamma ! ) ; } ; window . addEventListener ( 'deviceorientation' , handleOrientation ) ; } const handleMove = ( e ) => { if ( this . isPaused ) return ; let left = e . pageX ; let top = e . pageY ; const offset = this . getEventElemOffset ( ) ; if ( offset ) { left -= offset . left ; top -= offset . top ; } move ( left , top ) ; } ; eventElem ! . addEventListener ( 'mousemove' , handleMove as EventListener ) ; this . onDestroy ( ( ) => { window . removeEventListener ( 'deviceorientation' , handleOrientation ) ; eventElem ! . removeEventListener ( 'mousemove' , handleMove as EventListener ) ; } ) ; } /**
     * 鼠标位置事件，根据鼠标的坐标将范围内的粒子连接起来
     */ private positionEvent ( ) { const { range } = this . options ; if ( range > this . canvasWidth && range > this . canvasHeight ) return ; this . eventProxy ( ( left , top ) => { this . positionX = left ; this . positionY = top ; } , ( beta , gamma ) => { this . positionX = ( - ( gamma - 90 ) / 180 ) * this . canvasWidth ; this . positionY = ( - ( beta - 90 ) / 180 ) * this . canvasHeight ; } ) ; } /**
     * 视差效果事件
     */ private parallaxEvent ( ) { if ( ! this . options . parallax ) return ; this . eventProxy ( ( left , top ) => { this . mouseX = left - this . canvasWidth / 2 ; this . mouseY = top - this . canvasHeight / 2 ; } , ( beta , gamma ) => { this . mouseX = ( - gamma * this . canvasWidth ) / 180 ; this . mouseY = ( - beta * this . canvasHeight ) / 180 ; } ) ; } /**
     * 窗口尺寸调整事件
     */ private ownResizeEvent ( ) { this . onResize ( ( scaleX , scaleY ) => { if ( this . options . range > 0 ) { this . positionX ! *= scaleX ; this . positionY ! *= scaleY ; this . mouseX *= scaleX ; this . mouseY *= scaleY ; } } ) ; } }