import Lucky from './lucky' ; import { ConfigType } from '../types/index' ; import LuckyWheelConfig , { BlockType , BlockImgType , PrizeType , PrizeImgType , ButtonType , ButtonImgType , DefaultConfigType , DefaultStyleType , StartCallbackType , EndCallbackType } from '../types/wheel' ; import { FontType , ImgType , UniImageType } from '../types/index' ; import { isExpectType , removeEnter , hasBackground } from '../utils/index' ; import { getAngle , drawSector } from '../utils/math' ; import * as Tween from '../utils/tween' ; export default class LuckyWheel extends Lucky { private blocks : Array < BlockType > = [ ] ; private prizes : Array < PrizeType > = [ ] ; private buttons : Array < ButtonType > = [ ] ; private defaultConfig : DefaultConfigType = { } ; private _defaultConfig = { gutter : '0px' , offsetDegree : 0 , speed : 20 , speedFunction : 'quad' , accelerationTime : 2500 , decelerationTime : 2500 , stopRange : 0.8 , } ; private defaultStyle : DefaultStyleType = { } ; private _defaultStyle = { fontSize : '18px' , fontColor : '#000' , fontStyle : 'sans-serif' , fontWeight : '400' , lineHeight : '' , background : 'rgba(0,0,0,0)' , wordWrap : true , lengthLimit : '90%' , } ; private startCallback ? : StartCallbackType ; private endCallback ? : EndCallbackType ; private Radius = 0 ; private prizeRadius = 0 ; private prizeDeg = 0 ; private prizeRadian = 0 ; private rotateDeg = 0 ; private maxBtnRadius = 0 ; private startTime = 0 ; private endTime = 0 ; private stopDeg = 0 ; private endDeg = 0 ; private FPS = 16.6 ; /**
     * 中奖索引
     * prizeFlag = undefined 时, 处于开始抽奖阶段, 正常旋转
     * prizeFlag >= 0 时, 说明stop方法被调用, 并且传入了中奖索引
     * prizeFlag === -1 时, 说明stop方法被调用, 并且传入了负值, 本次抽奖无效
     */ private prizeFlag : number | undefined ; private blockImgs : Array < HTMLImageElement [ ] | UniImageType [ ] > = [ [ ] ] ; private prizeImgs : Array < HTMLImageElement [ ] | UniImageType [ ] > = [ [ ] ] ; private btnImgs : Array < HTMLImageElement [ ] | UniImageType [ ] > = [ [ ] ] ; param config param data constructor ( config , data = { } ) { super ( config ) ; if ( config . ob ) { this . initData ( data ) ; this . initWatch ( ) ; } this . initComputed ( ) ; config . beforeCreate ?. call ( this ) ; this . init ( { blockImgs : this . blocks . map ( block => block . imgs ) , prizeImgs : this . prizes . map ( prize => prize . imgs ) , btnImgs : this . buttons . map ( btn => btn . imgs ) , } ) ; } param data private initData ( data ) { this . $set ( this , 'blocks' , data . blocks || [ ] ) ; this . $set ( this , 'prizes' , data . prizes || [ ] ) ; this . $set ( this , 'buttons' , data . buttons || [ ] ) ; this . $set ( this , 'defaultConfig' , data . defaultConfig || { } ) ; this . $set ( this , 'defaultStyle' , data . defaultStyle || { } ) ; this . $set ( this , 'startCallback' , data . start ) ; this . $set ( this , 'endCallback' , data . end ) ; } /**
     * 初始化属性计算
     */ private initComputed ( ) { this . $computed ( this , '_defaultConfig' , ( ) => { const config = { gutter : '0px' , offsetDegree : 0 , speed : 20 , speedFunction : 'quad' , accelerationTime : 2500 , decelerationTime : 2500 , stopRange : 0.8 , ... this . defaultConfig } ; return config ; } ) ; this . $computed ( this , '_defaultStyle' , ( ) => { const style = { fontSize : '18px' , fontColor : '#000' , fontStyle : 'sans-serif' , fontWeight : '400' , background : 'rgba(0,0,0,0)' , wordWrap : true , lengthLimit : '90%' , ... this . defaultStyle } ; return style ; } ) ; } /**
     * 初始化观察者
     */ private initWatch ( ) { this . $watch ( 'blocks' , ( newData ) => { return this . init ( { blockImgs : newData . map ( cell => cell . imgs ) } ) ; } , { deep : true } ) ; this . $watch ( 'prizes' , ( newData ) => { return this . init ( { prizeImgs : newData . map ( cell => cell . imgs ) } ) ; } , { deep : true } ) ; this . $watch ( 'buttons' , ( newData ) => { return this . init ( { btnImgs : newData . map ( cell => cell . imgs ) } ) ; } , { deep : true } ) ; this . $watch ( 'defaultConfig' , ( ) => this . draw ( ) , { deep : true } ) ; this . $watch ( 'defaultStyle' , ( ) => this . draw ( ) , { deep : true } ) ; this . $watch ( 'startCallback' , ( ) => this . init ( { } ) ) ; this . $watch ( 'endCallback' , ( ) => this . init ( { } ) ) ; } param { willUpdateImgs } willUpdateImgs public init ( willUpdateImgs ) { super . init ( ) ; const { config , ctx } = this ; this . Radius = Math . min ( this . boxWidth , this . boxHeight ) / 2 ; config . beforeInit ?. call ( this ) ; ctx . translate ( this . Radius , this . Radius ) ; this . draw ( ) ; this . draw ( ) ; Object . keys ( willUpdateImgs ) . forEach ( key => { const imgName = key as 'blockImgs' | 'prizeImgs' | 'btnImgs' ; const cellName = { blockImgs : 'blocks' , prizeImgs : 'prizes' , btnImgs : 'buttons' , } [ imgName ] as 'blocks' | 'prizes' | 'buttons' ; const willUpdate = willUpdateImgs [ imgName ] ; 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 ; ctx . beginPath ( ) ; ctx . arc ( 0 , 0 , this . maxBtnRadius , 0 , Math . PI * 2 , false ) ; if ( ! ctx . isPointInPath ( e . offsetX , e . offsetY ) ) return ; if ( this . startTime ) return ; this . startCallback ?. ( e ) ; } param { number } cellIndex param { number } imgIndex param { Function } callBack private async loadAndCacheImg ( cellName , cellIndex , imgName , imgIndex , callBack ) { const cell = this [ cellName ] [ cellIndex ] ; if ( ! cell || ! cell . imgs ) return ; const imgInfo = cell . imgs [ imgIndex ] ; if ( ! imgInfo ) return ; if ( ! this [ imgName ] [ cellIndex ] ) this [ imgName ] [ cellIndex ] = [ ] ; this . loadImg ( imgInfo . src , imgInfo ) . then ( res => { this [ imgName ] [ cellIndex ] [ imgIndex ] = res ; callBack . call ( this ) ; } ) . catch ( err => { console . error ( `${ cellName }[${ cellIndex }].imgs[${ imgIndex }] ${ err }` ) ; } ) ; } param imgObj param imgInfo param maxWidth param maxHeight return private computedWidthAndHeight ( imgObj , imgInfo , maxWidth , maxHeight ) { if ( ! imgInfo . width && ! imgInfo . height ) { return [ imgObj . width , imgObj . height ] ; } else if ( imgInfo . width && ! imgInfo . height ) { let trueWidth = this . getWidth ( imgInfo . width , maxWidth ) ; return [ trueWidth , imgObj . height * ( trueWidth / imgObj . width ) ] ; } else if ( ! imgInfo . width && imgInfo . height ) { let trueHeight = this . getHeight ( imgInfo . height , maxHeight ) ; return [ imgObj . width * ( trueHeight / imgObj . height ) , trueHeight ] ; } return [ this . getWidth ( imgInfo . width , maxWidth ) , this . getHeight ( imgInfo . height , maxHeight ) ] ; } /**
     * 开始绘制
     */ protected draw ( ) { const { config , ctx , _defaultConfig , _defaultStyle } = this ; config . beforeDraw ?. call ( this , ctx ) ; ctx . clearRect ( - this . Radius , - this . Radius , this . Radius * 2 , this . Radius * 2 ) ; this . prizeRadius = this . blocks . reduce ( ( radius , block , blockIndex ) => { if ( hasBackground ( block . background ) ) { ctx . beginPath ( ) ; ctx . fillStyle = block . background ! ; ctx . arc ( 0 , 0 , radius , 0 , Math . PI * 2 , false ) ; ctx . fill ( ) ; } block . imgs && block . imgs . forEach ( ( imgInfo , imgIndex ) => { if ( ! this . blockImgs [ blockIndex ] ) return ; const blockImg = this . blockImgs [ blockIndex ] [ imgIndex ] ; if ( ! blockImg ) return ; const [ trueWidth , trueHeight ] = this . computedWidthAndHeight ( blockImg , imgInfo , radius * 2 , radius * 2 ) ; const [ xAxis , yAxis ] = [ this . getOffsetX ( trueWidth ) , this . getHeight ( imgInfo . top , radius * 2 ) - radius ] ; ctx . save ( ) ; imgInfo . rotate && ctx . rotate ( getAngle ( this . rotateDeg ) ) ; this . drawImage ( blockImg , xAxis , yAxis , trueWidth , trueHeight ) ; ctx . restore ( ) ; } ) ; return radius - this . getLength ( block . padding && block . padding . split ( ' ' ) [ 0 ] ) ; } , this . Radius ) ; this . prizeDeg = 360 / this . prizes . length ; this . prizeRadian = getAngle ( this . prizeDeg ) ; let start = getAngle ( - 90 + this . rotateDeg + _defaultConfig . offsetDegree ) ; const getFontX = ( line ) => { return this . getOffsetX ( ctx . measureText ( line ) . width ) ; } ; const getFontY = ( font , height , lineIndex ) => { const lineHeight = font . lineHeight || _defaultStyle . lineHeight || font . fontSize || _defaultStyle . fontSize ; return this . getHeight ( font . top , height ) + ( lineIndex + 1 ) * this . getLength ( lineHeight ) ; } ; ctx . save ( ) ; this . prizes . forEach ( ( prize , prizeIndex ) => { let currMiddleDeg = start + prizeIndex * this . prizeRadian ; let prizeHeight = this . prizeRadius - this . maxBtnRadius ; const background = prize . background || _defaultStyle . background ; hasBackground ( background ) && drawSector ( config . flag , ctx , this . maxBtnRadius , this . prizeRadius , currMiddleDeg - this . prizeRadian / 2 , currMiddleDeg + this . prizeRadian / 2 , this . getLength ( _defaultConfig . gutter ) , background ) ; let x = Math . cos ( currMiddleDeg ) * this . prizeRadius ; let y = Math . sin ( currMiddleDeg ) * this . prizeRadius ; ctx . translate ( x , y ) ; ctx . rotate ( currMiddleDeg + getAngle ( 90 ) ) ; prize . imgs && prize . imgs . forEach ( ( imgInfo , imgIndex ) => { if ( ! this . prizeImgs [ prizeIndex ] ) return ; const prizeImg = this . prizeImgs [ prizeIndex ] [ imgIndex ] ; if ( ! prizeImg ) return ; const [ trueWidth , trueHeight ] = this . computedWidthAndHeight ( prizeImg , imgInfo , this . prizeRadian * this . prizeRadius , prizeHeight ) ; const [ xAxis , yAxis ] = [ this . getOffsetX ( trueWidth ) , this . getHeight ( imgInfo . top , prizeHeight ) ] ; this . drawImage ( prizeImg , xAxis , yAxis , trueWidth , trueHeight ) ; } ) ; prize . fonts && prize . fonts . forEach ( font => { let fontColor = font . fontColor || _defaultStyle . fontColor ; let fontWeight = font . fontWeight || _defaultStyle . fontWeight ; let fontSize = this . getLength ( font . fontSize || _defaultStyle . fontSize ) ; let fontStyle = font . fontStyle || _defaultStyle . fontStyle ; ctx . fillStyle = fontColor ! ; ctx . font = `${ fontWeight } ${ fontSize >> 0 }px ${ fontStyle }` ; 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 . prizeRadius - getFontY ( font , prizeHeight , lines . length ) ) * Math . tan ( this . prizeRadian / 2 ) * 2 - this . getLength ( _defaultConfig . gutter ) ; if ( currWidth > this . getWidth ( font . lengthLimit || _defaultStyle . lengthLimit , 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 . filter ( line => ! ! line ) . forEach ( ( line , lineIndex ) => { ctx . fillText ( line , getFontX ( line ) , getFontY ( font , prizeHeight , lineIndex ) ) ; } ) ; } ) ; ctx . rotate ( getAngle ( 360 ) - currMiddleDeg - getAngle ( 90 ) ) ; ctx . translate ( - x , - y ) ; } ) ; ctx . restore ( ) ; this . buttons . forEach ( ( btn , btnIndex ) => { let radius = this . getHeight ( btn . radius ) ; this . maxBtnRadius = Math . max ( this . maxBtnRadius , radius ) ; if ( hasBackground ( btn . background ) ) { ctx . beginPath ( ) ; ctx . fillStyle = btn . background as string ; ctx . arc ( 0 , 0 , radius , 0 , Math . PI * 2 , false ) ; ctx . fill ( ) ; } if ( btn . pointer && hasBackground ( btn . background ) ) { ctx . beginPath ( ) ; ctx . fillStyle = btn . background as string ; ctx . moveTo ( - radius , 0 ) ; ctx . lineTo ( radius , 0 ) ; ctx . lineTo ( 0 , - radius * 2 ) ; ctx . closePath ( ) ; ctx . fill ( ) ; } btn . imgs && btn . imgs . forEach ( ( imgInfo , imgIndex ) => { if ( ! this . btnImgs [ btnIndex ] ) return ; const btnImg = this . btnImgs [ btnIndex ] [ imgIndex ] ; if ( ! btnImg ) return ; const [ trueWidth , trueHeight ] = this . computedWidthAndHeight ( btnImg , imgInfo , radius * 2 , radius * 2 ) ; const [ xAxis , yAxis ] = [ this . getOffsetX ( trueWidth ) , this . getHeight ( imgInfo . top , radius ) ] ; this . drawImage ( btnImg , xAxis , yAxis , trueWidth , trueHeight ) ; } ) ; btn . fonts && btn . fonts . forEach ( font => { let fontColor = font . fontColor || _defaultStyle . fontColor ; let fontWeight = font . fontWeight || _defaultStyle . fontWeight ; let fontSize = this . getLength ( font . fontSize || _defaultStyle . fontSize ) ; let fontStyle = font . fontStyle || _defaultStyle . fontStyle ; ctx . fillStyle = fontColor ! ; ctx . font = `${ fontWeight } ${ fontSize >> 0 }px ${ fontStyle }` ; String ( font . text ) . split ( '\n' ) . forEach ( ( line , lineIndex ) => { ctx . fillText ( line , getFontX ( line ) , getFontY ( font , radius , lineIndex ) ) ; } ) ; } ) ; } ) ; config . afterDraw ?. call ( this , ctx ) ; } /**
     * 对外暴露: 开始抽奖方法
     */ public play ( ) { if ( this . startTime ) return ; 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 . rotateDeg = this . prizeDeg / 2 - this . _defaultConfig . offsetDegree ; this . draw ( ) ; } } param num private run ( num = 0 ) { const { rAF , prizeFlag , prizeDeg , rotateDeg , _defaultConfig } = this ; if ( prizeFlag === - 1 ) return ( this . startTime = 0 , void 0 ) ; let interval = Date . now ( ) - this . startTime ; if ( interval >= _defaultConfig . accelerationTime && prizeFlag !== void 0 ) { this . FPS = interval / num ; this . endTime = Date . now ( ) ; this . stopDeg = rotateDeg ; const stopRange = ( Math . random ( ) * prizeDeg - prizeDeg / 2 ) * this . getLength ( _defaultConfig . stopRange ) ; let i = 0 ; while ( ++ i ) { const endDeg = 360 * i - prizeFlag * prizeDeg - rotateDeg - _defaultConfig . offsetDegree + stopRange ; let currSpeed = Tween [ _defaultConfig . speedFunction ] . easeOut ( this . FPS , this . stopDeg , endDeg , _defaultConfig . decelerationTime ) - this . stopDeg ; if ( currSpeed > _defaultConfig . speed ) { this . endDeg = endDeg ; break ; } } return this . slowDown ( ) ; } this . rotateDeg = ( rotateDeg + Tween [ _defaultConfig . speedFunction ] . easeIn ( interval , 0 , _defaultConfig . speed , _defaultConfig . accelerationTime ) ) % 360 ; this . draw ( ) ; rAF ( this . run . bind ( this , num + 1 ) ) ; } /**
     * 缓慢停止的方法
     */ private slowDown ( ) { const { rAF , prizes , prizeFlag , stopDeg , endDeg , _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 . rotateDeg = Tween [ _defaultConfig . speedFunction ] . easeOut ( interval , stopDeg , endDeg , _defaultConfig . decelerationTime ) % 360 ; this . draw ( ) ; rAF ( this . slowDown . bind ( this ) ) ; } param length param width return private getWidth ( length , width = this . prizeRadian * this . prizeRadius ) { if ( isExpectType ( length , 'number' ) ) return ( length as number ) ; if ( isExpectType ( length , 'string' ) ) return this . changeUnits ( length as string , width ) ; return 0 ; } param length param height return private getHeight ( length , height = this . prizeRadius ) { if ( isExpectType ( length , 'number' ) ) return ( length as number ) ; if ( isExpectType ( length , 'string' ) ) return this . changeUnits ( length as string , height ) ; return 0 ; } param width return private getOffsetX ( width ) { return - width / 2 ; } param x param y protected conversionAxis ( x , y ) { const { config } = this ; return [ x / config . dpr - this . Radius , y / config . dpr - this . Radius ] ; } }