import Lucky from './lucky' ; import { ConfigType , UniImageType } from '../types/index' ; import LuckyGridConfig , { BlockType , BlockImgType , PrizeType , PrizeImgType , ButtonType , ButtonImgType , CellFontType , CellImgType , RowsType , ColsType , CellType , DefaultConfigType , DefaultStyleType , ActiveStyleType , StartCallbackType , EndCallbackType , } from '../types/grid' ; import { isExpectType , removeEnter , computePadding , hasBackground } from '../utils/index' ; import { drawRoundRect , getLinearGradient } from '../utils/math' ; import { quad } from '../utils/tween' ; type ImgObjType = HTMLImageElement | UniImageType ; export default class LuckyGrid extends Lucky { private rows : RowsType = 3 ; private cols : ColsType = 3 ; private blocks : Array < BlockType > = [ ] ; private prizes : Array < PrizeType > = [ ] ; private buttons : Array < ButtonType > = [ ] ; private button ? : ButtonType ; private defaultConfig : DefaultConfigType = { } ; private _defaultConfig = { gutter : 5 , speed : 20 , accelerationTime : 2500 , decelerationTime : 2500 , } ; private defaultStyle : DefaultStyleType = { } ; private _defaultStyle = { borderRadius : 20 , fontColor : '#000' , fontSize : '18px' , fontStyle : 'sans-serif' , fontWeight : '400' , lineHeight : '' , background : 'rgba(0,0,0,0)' , shadow : '' , wordWrap : true , lengthLimit : '90%' , } ; private activeStyle : ActiveStyleType = { } ; private _activeStyle = { background : '#ffce98' , shadow : '' , fontStyle : '' , fontWeight : '' , fontSize : '' , lineHeight : '' , fontColor : '' , } ; private startCallback ? : StartCallbackType ; private endCallback ? : EndCallbackType ; private cellWidth = 0 ; private cellHeight = 0 ; private startTime = 0 ; private endTime = 0 ; private currIndex = 0 ; private stopIndex = 0 ; private endIndex = 0 ; private demo = false ; private timer = 0 ; private FPS = 16.6 ; /**
     * 中奖索引
     * prizeFlag = undefined 时, 处于开始抽奖阶段, 正常旋转
     * prizeFlag >= 0 时, 说明stop方法被调用, 并且传入了中奖索引
     * prizeFlag === -1 时, 说明stop方法被调用, 并且传入了负值, 本次抽奖无效
     */ private prizeFlag : number | undefined = - 1 ; private cells : CellType < CellFontType , CellImgType > [ ] = [ ] ; private prizeArea : { x : number ; y : number ; w : number ; h : number ; } | undefined ; private blockImgs : Array < { defaultImg : ImgObjType ; } [ ] > = [ [ ] ] ; private btnImgs : Array < { defaultImg : ImgObjType ; } [ ] > = [ [ ] ] ; private prizeImgs : Array < { defaultImg : ImgObjType ; activeImg ? : ImgObjType ; } [ ] > = [ ] ; param config param data constructor ( config , data = { } ) { super ( config ) ; if ( config . ob ) { this . initData ( data ) ; this . initWatch ( ) ; } this . initComputed ( ) ; config . beforeCreate ?. call ( this ) ; const btnImgs = this . buttons . map ( btn => btn . imgs ) ; if ( this . button ) btnImgs . push ( this . button . imgs ) ; this . init ( { blockImgs : this . blocks . map ( block => block . imgs ) , prizeImgs : this . prizes . map ( prize => prize . imgs ) , btnImgs , } ) ; } param data private initData ( data ) { this . $set ( this , 'rows' , Number ( data . rows ) || 3 ) ; this . $set ( this , 'cols' , Number ( data . cols ) || 3 ) ; this . $set ( this , 'blocks' , data . blocks || [ ] ) ; this . $set ( this , 'prizes' , data . prizes || [ ] ) ; this . $set ( this , 'buttons' , data . buttons || [ ] ) ; this . $set ( this , 'button' , data . button ) ; this . $set ( this , 'defaultConfig' , data . defaultConfig || { } ) ; this . $set ( this , 'defaultStyle' , data . defaultStyle || { } ) ; this . $set ( this , 'activeStyle' , data . activeStyle || { } ) ; this . $set ( this , 'startCallback' , data . start ) ; this . $set ( this , 'endCallback' , data . end ) ; } /**
     * 初始化属性计算
     */ private initComputed ( ) { this . $computed ( this , '_defaultConfig' , ( ) => { const config = { gutter : 5 , speed : 20 , accelerationTime : 2500 , decelerationTime : 2500 , ... this . defaultConfig } ; config . gutter = this . getLength ( config . gutter ) ; config . speed = config . speed / 40 ; return config ; } ) ; this . $computed ( this , '_defaultStyle' , ( ) => { return { borderRadius : 20 , fontColor : '#000' , fontSize : '18px' , fontStyle : 'sans-serif' , fontWeight : '400' , background : 'rgba(0,0,0,0)' , shadow : '' , wordWrap : true , lengthLimit : '90%' , ... this . defaultStyle } ; } ) ; this . $computed ( this , '_activeStyle' , ( ) => { return { background : '#ffce98' , shadow : '' , ... this . activeStyle } ; } ) ; } /**
     * 初始化观察者
     */ private initWatch ( ) { this . $watch ( 'blocks' , ( newData ) => { return this . init ( { blockImgs : newData . map ( block => block . imgs ) } ) ; } , { deep : true } ) ; this . $watch ( 'prizes' , ( newData ) => { return this . init ( { prizeImgs : newData . map ( prize => prize . imgs ) } ) ; } , { deep : true } ) ; this . $watch ( 'buttons' , ( newData ) => { const btnImgs = newData . map ( btn => btn . imgs ) ; if ( this . button ) btnImgs . push ( this . button . imgs ) ; return this . init ( { btnImgs } ) ; } , { deep : true } ) ; this . $watch ( 'button' , ( ) => { const btnImgs = this . buttons . map ( btn => btn . imgs ) ; if ( this . button ) btnImgs . push ( this . button . imgs ) ; return this . init ( { btnImgs } ) ; } , { deep : true } ) ; this . $watch ( 'rows' , ( ) => this . init ( { } ) ) ; this . $watch ( 'cols' , ( ) => this . init ( { } ) ) ; this . $watch ( 'defaultConfig' , ( ) => this . draw ( ) , { deep : true } ) ; this . $watch ( 'defaultStyle' , ( ) => this . draw ( ) , { deep : true } ) ; this . $watch ( 'activeStyle' , ( ) => this . draw ( ) , { deep : true } ) ; this . $watch ( 'startCallback' , ( ) => this . init ( { } ) ) ; this . $watch ( 'endCallback' , ( ) => this . init ( { } ) ) ; } param willUpdateImgs public init ( willUpdateImgs ) { super . init ( ) ; const { config , ctx , button } = this ; config . beforeInit ?. call ( this ) ; this . draw ( ) ; Object . keys ( willUpdateImgs ) . forEach ( key => { const imgName = key as 'blockImgs' | 'prizeImgs' | 'btnImgs' ; const willUpdate = willUpdateImgs [ imgName ] ; const cellName = { blockImgs : 'blocks' , prizeImgs : 'prizes' , btnImgs : 'buttons' , } [ imgName ] as 'blocks' | 'prizes' | 'buttons' ; willUpdate && willUpdate . forEach ( ( imgs , cellIndex ) => { imgs && imgs . forEach ( ( imgInfo , imgIndex ) => { this . loadAndCacheImg ( cellName , cellIndex , imgName , imgIndex , ( ) => { this . draw ( ) ; } ) ; } ) ; } ) ; } ) ; config . afterInit ?. call ( this ) ; } param e protected handleClick ( e ) { const { ctx } = this ; [ ... this . buttons , this . button ] . forEach ( btn => { if ( ! btn ) return ; const [ x , y , width , height ] = this . getGeometricProperty ( [ btn . x , btn . y , btn . col || 1 , btn . row || 1 ] ) ; ctx . beginPath ( ) ; ctx . rect ( x , y , width , height ) ; if ( ! ctx . isPointInPath ( e . offsetX , e . offsetY ) ) return ; if ( this . startTime ) return ; if ( typeof btn . callback === 'function' ) btn . callback . call ( this , btn ) ; this . startCallback ?. ( e , btn ) ; } ) ; } param { number } prizeIndex param { number } imgIndex param { Function } callBack private async loadAndCacheImg ( cellName , cellIndex , imgName , imgIndex , callBack ) { let cell = this [ cellName ] [ cellIndex ] ; if ( cellName === 'buttons' && ! this . buttons . length && this . button ) { cell = this . button ; } if ( ! cell || ! cell . imgs ) return ; const imgInfo = cell . imgs [ imgIndex ] ; if ( ! imgInfo ) return ; if ( ! this [ imgName ] [ cellIndex ] ) this [ imgName ] [ cellIndex ] = [ ] ; const request = [ this . loadImg ( imgInfo . src , imgInfo ) , imgInfo [ 'activeSrc' ] && this . loadImg ( imgInfo [ 'activeSrc' ] , imgInfo , '$activeResolve' ) ] ; Promise . all ( request ) . then ( ( [ defaultImg , activeImg ] ) => { this [ imgName ] [ cellIndex ] [ imgIndex ] = { defaultImg , activeImg } as { defaultImg : ImgObjType ; activeImg ? : ImgObjType ; } ; callBack . call ( this ) ; } ) . catch ( err => { console . error ( `${ cellName }[${ cellIndex }].imgs[${ imgIndex }] ${ err }` ) ; } ) ; } param imgObj param imgInfo param cell return private computedWidthAndHeight ( imgObj , imgInfo , cell ) { if ( ! imgInfo . width && ! imgInfo . height ) { return [ imgObj . width , imgObj . height ] ; } else if ( imgInfo . width && ! imgInfo . height ) { let trueWidth = this . getWidth ( imgInfo . width , cell . col ) ; return [ trueWidth , imgObj . height * ( trueWidth / imgObj . width ) ] ; } else if ( ! imgInfo . width && imgInfo . height ) { let trueHeight = this . getHeight ( imgInfo . height , cell . row ) ; return [ imgObj . width * ( trueHeight / imgObj . height ) , trueHeight ] ; } return [ this . getWidth ( imgInfo . width , cell . col ) , this . getHeight ( imgInfo . height , cell . row ) ] ; } /**
     * 绘制九宫格抽奖
     */ protected draw ( ) { const { config , ctx , _defaultConfig , _defaultStyle , _activeStyle } = this ; config . beforeDraw ?. call ( this , ctx ) ; ctx . clearRect ( 0 , 0 , this . boxWidth , this . boxHeight ) ; this . cells = [ ... this . prizes , ... this . buttons ] ; if ( this . button ) this . cells . push ( this . button ) ; this . cells . forEach ( cell => { cell . col = cell . col || 1 ; cell . row = cell . row || 1 ; } ) ; this . prizeArea = this . blocks . reduce ( ( { x , y , w , h } , block ) => { const [ paddingTop , paddingBottom , paddingLeft , paddingRight ] = computePadding ( block ) ; const r = block . borderRadius ? this . getLength ( block . borderRadius ) : 0 ; const background = block . background || _defaultStyle . background ; if ( hasBackground ( background ) ) { drawRoundRect ( ctx , x , y , w , h , r , this . handleBackground ( x , y , w , h , background ) ) ; } return { x : x + paddingLeft , y : y + paddingTop , w : w - paddingLeft - paddingRight , h : h - paddingTop - paddingBottom } ; } , { x : 0 , y : 0 , w : this . boxWidth , h : this . boxHeight } ) ; this . cellWidth = ( this . prizeArea . w - _defaultConfig . gutter * ( this . cols - 1 ) ) / this . cols ; this . cellHeight = ( this . prizeArea . h - _defaultConfig . gutter * ( this . rows - 1 ) ) / this . rows ; this . cells . forEach ( ( cell , cellIndex ) => { let [ x , y , width , height ] = this . getGeometricProperty ( [ cell . x , cell . y , cell . col , cell . row ] ) ; let isActive = false ; if ( this . prizeFlag === void 0 || this . prizeFlag > - 1 ) { isActive = cellIndex === this . currIndex % this . prizes . length >> 0 ; } const background = isActive ? _activeStyle . background : ( cell . background || _defaultStyle . background ) ; if ( hasBackground ( background ) ) { const shadow = ( isActive ? _activeStyle . shadow ! : ( cell . shadow || _defaultStyle . shadow ! ) ) . replace ( /px/g , '' ) . split ( ',' ) [ 0 ] . split ( ' ' ) . map ( ( n , i ) => i < 3 ? Number ( n ) : n ) ; if ( shadow . length === 4 ) { ctx . shadowColor = shadow [ 3 ] ; ctx . shadowOffsetX = shadow [ 0 ] * config . dpr ; ctx . shadowOffsetY = shadow [ 1 ] * config . dpr ; ctx . shadowBlur = shadow [ 2 ] ; shadow [ 0 ] > 0 ? ( width -= shadow [ 0 ] ) : ( width += shadow [ 0 ] , x -= shadow [ 0 ] ) ; shadow [ 1 ] > 0 ? ( height -= shadow [ 1 ] ) : ( height += shadow [ 1 ] , y -= shadow [ 1 ] ) ; } drawRoundRect ( ctx , x , y , width , height , this . getLength ( cell . borderRadius ? cell . borderRadius : _defaultStyle . borderRadius ) , this . handleBackground ( x , y , width , height , background ) ) ; ctx . shadowColor = 'rgba(0, 0, 0, 0)' ; ctx . shadowOffsetX = 0 ; ctx . shadowOffsetY = 0 ; ctx . shadowBlur = 0 ; } let cellName = 'prizeImgs' ; if ( cellIndex >= this . prizes . length ) { cellName = 'btnImgs' ; cellIndex -= this . prizes . length ; } cell . imgs && cell . imgs . forEach ( ( imgInfo , imgIndex ) => { if ( ! this [ cellName ] [ cellIndex ] ) return ; const cellImg = this [ cellName ] [ cellIndex ] [ imgIndex ] ; if ( ! cellImg ) return ; const renderImg = ( isActive && cellImg [ 'activeImg' ] ) || cellImg . defaultImg ; if ( ! renderImg ) return ; const [ trueWidth , trueHeight ] = this . computedWidthAndHeight ( renderImg , imgInfo , cell ) ; const [ xAxis , yAxis ] = [ x + this . getOffsetX ( trueWidth , cell . col ) , y + this . getHeight ( imgInfo . top , cell . row ) ] ; this . drawImage ( renderImg , xAxis , yAxis , trueWidth , trueHeight ) ; } ) ; cell . fonts && cell . fonts . forEach ( font => { let style = isActive && _activeStyle . fontStyle ? _activeStyle . fontStyle : ( font . fontStyle || _defaultStyle . fontStyle ) ; let fontWeight = isActive && _activeStyle . fontWeight ? _activeStyle . fontWeight : ( font . fontWeight || _defaultStyle . fontWeight ) ; let size = isActive && _activeStyle . fontSize ? this . getLength ( _activeStyle . fontSize ) : this . getLength ( font . fontSize || _defaultStyle . fontSize ) ; const lineHeight = isActive && _activeStyle . lineHeight ? _activeStyle . lineHeight : font . lineHeight || _defaultStyle . lineHeight || font . fontSize || _defaultStyle . fontSize ; ctx . font = `${ fontWeight } ${ size >> 0 }px ${ style }` ; ctx . fillStyle = ( isActive && _activeStyle . fontColor ) ? _activeStyle . fontColor : ( font . fontColor || _defaultStyle . fontColor ) ; let lines = [ ] , text = String ( font . text ) ; if ( Object . prototype . hasOwnProperty . call ( font , 'wordWrap' ) ? font . wordWrap : _defaultStyle . wordWrap ) { text = removeEnter ( text ) ; let str = '' ; for ( let i = 0 ; i < text . length ; i ++ ) { str += text [ i ] ; let currWidth = ctx . measureText ( str ) . width ; let maxWidth = this . getWidth ( font . lengthLimit || _defaultStyle . lengthLimit , cell . col ) ; if ( currWidth > maxWidth ) { lines . push ( str . slice ( 0 , - 1 ) ) ; str = text [ i ] ; } } if ( str ) lines . push ( str ) ; if ( ! lines . length ) lines . push ( text ) ; } else { lines = text . split ( '\n' ) ; } lines . forEach ( ( line , lineIndex ) => { ctx . fillText ( line , x + this . getOffsetX ( ctx . measureText ( line ) . width , cell . col ) , y + this . getHeight ( font . top , cell . row ) + ( lineIndex + 1 ) * this . getLength ( lineHeight ) ) ; } ) ; } ) ; } ) ; config . afterDraw ?. call ( this , ctx ) ; } param x param y param width param height param background param isActive private handleBackground ( x , y , width , height , background ) { const { ctx } = this ; if ( background . includes ( 'linear-gradient' ) ) { background = getLinearGradient ( ctx , x , y , width , height , background ) ; } return background ; } /**
     * 对外暴露: 开始抽奖方法
     */ public play ( ) { const { clearInterval } = this . config ; if ( this . startTime ) return ; clearInterval ( this . timer ) ; this . startTime = Date . now ( ) ; this . prizeFlag = void 0 ; this . run ( ) ; } param index public stop ( index ) { this . prizeFlag = index < 0 ? - 1 : index % this . prizes . length ; if ( this . prizeFlag === - 1 ) { this . currIndex = 0 ; this . draw ( ) ; } } param num private run ( num = 0 ) { const { rAF , currIndex , prizes , prizeFlag , startTime , _defaultConfig } = this ; let interval = Date . now ( ) - startTime ; if ( interval >= _defaultConfig . accelerationTime && prizeFlag !== void 0 ) { this . FPS = interval / num ; this . endTime = Date . now ( ) ; this . stopIndex = currIndex ; let i = 0 ; while ( ++ i ) { const endIndex = prizes . length * i + prizeFlag - ( currIndex >> 0 ) ; let currSpeed = quad . easeOut ( this . FPS , this . stopIndex , endIndex , _defaultConfig . decelerationTime ) - this . stopIndex ; if ( currSpeed > _defaultConfig . speed ) { this . endIndex = endIndex ; break ; } } return this . slowDown ( ) ; } this . currIndex = ( currIndex + quad . easeIn ( interval , 0.1 , _defaultConfig . speed , _defaultConfig . accelerationTime ) ) % prizes . length ; this . draw ( ) ; rAF ( this . run . bind ( this , num + 1 ) ) ; } /**
     * 缓慢停止的方法
     */ private slowDown ( ) { const { rAF , prizes , prizeFlag , stopIndex , endIndex , _defaultConfig } = this ; let interval = Date . now ( ) - this . endTime ; if ( prizeFlag === - 1 ) return ( this . startTime = 0 , void 0 ) ; if ( interval > _defaultConfig . decelerationTime ) { this . startTime = 0 ; this . endCallback ?. ( { ... prizes . find ( ( prize , index ) => index === prizeFlag ) } ) ; return ; } this . currIndex = quad . easeOut ( interval , stopIndex , endIndex , _defaultConfig . decelerationTime ) % prizes . length ; this . draw ( ) ; rAF ( this . slowDown . bind ( this ) ) ; } /**
     * 开启中奖标识自动游走
     */ public walk ( ) { const { setInterval , clearInterval } = this . config ; clearInterval ( this . timer ) ; this . timer = setInterval ( ( ) => { this . currIndex += 1 ; this . draw ( ) ; } , 1300 ) ; } param { array } . . 矩阵坐标 return { array } private getGeometricProperty ( [ x , y , col , row ] ) { const { cellWidth , cellHeight } = this ; const gutter = this . _defaultConfig . gutter ; let res = [ this . prizeArea ! . x + ( cellWidth + gutter ) * x , this . prizeArea ! . y + ( cellHeight + gutter ) * y ] ; col && row && res . push ( cellWidth * col + gutter * ( col - 1 ) , cellHeight * row + gutter * ( row - 1 ) ) ; return res ; } param width param col return private getWidth ( width , col = 1 ) { if ( isExpectType ( width , 'number' ) ) return ( width as number ) ; if ( isExpectType ( width , 'string' ) ) return this . changeUnits ( width as string , this . cellWidth * col + this . _defaultConfig . gutter * ( col - 1 ) ) ; return 0 ; } param height param row return private getHeight ( height , row = 1 ) { if ( isExpectType ( height , 'number' ) ) return ( height as number ) ; if ( isExpectType ( height , 'string' ) ) return this . changeUnits ( height as string , this . cellHeight * row + this . _defaultConfig . gutter * ( row - 1 ) ) ; return 0 ; } param width param col private getOffsetX ( width , col = 1 ) { return ( this . cellWidth * col + this . _defaultConfig . gutter * ( col - 1 ) - width ) / 2 ; } param x param y protected conversionAxis ( x , y ) { const { config } = this ; return [ x / config . dpr , y / config . dpr ] ; } }