import { include , extend , throwError , getSize , is , throwWarn , belowIOS8 , TGetSizeImage , forin , transValue , getLength , splitWords , } from '@Src/utils' ; import { Canvas } from '@Src/canvas' ; import { crop as cropFn } from '@Src/utils/crop' ; export class MCanvas { private ops : Required < TCanvas . options > ; private cvs : HTMLCanvasElement ; private ctx : CanvasRenderingContext2D ; private queue : TCanvas . queue = [ ] ; private fn = { success ( ) { } , error ( err ) { } , } ; private data : TCanvas . data = { textId : 0 , text : { } , bgConfig : null , } ; constructor ( options = { } ) { this . ops = extend ( { width : 500 , height : 500 , backgroundColor : '' , } , options ) ; this . _init ( ) ; } private _init ( ) { const { width , height , backgroundColor } = this . ops ; [ this . cvs , this . ctx ] = Canvas . create ( width , height ) ; backgroundColor && this . _setBgColor ( backgroundColor ) ; } public background ( image ? , bg = { type : 'origin' } ) { if ( ! image && ! this . data . bgConfig ) { throwError ( 'the init background must has a image.' ) ; return this ; } if ( image ) { bg . image = image ; this . data . bgConfig = bg ; } else if ( this . data . bgConfig ) { bg = this . data . bgConfig ; } this . queue . push ( ( ) => { if ( bg . color ) this . _setBgColor ( bg . color ) ; Canvas . getImage ( bg . image ) . then ( img => this . _background ( img , bg ) ) . catch ( this . fn . error ) ; } ) ; return this ; } private _setBgColor ( color ) { this . ctx . fillStyle = color ; this . ctx . fillRect ( 0 , 0 , this . cvs . width , this . cvs . height ) ; } private _getBgAlign ( left , iw , cw , cropScale ) { let rv ; if ( is . str ( left ) ) { if ( left === '50%' || left === 'center' ) { rv = Math . abs ( ( iw - cw / cropScale ) / 2 ) ; } else if ( left === '100%' ) { rv = Math . abs ( iw - cw / cropScale ) ; } else if ( left === '0%' ) { rv = 0 ; } } else if ( is . num ( left ) ) { rv = left ; } else { rv = 0 ; } return rv ; } private _background ( img , bg ) { const { iw , ih } = getSize ( img ) ; const iRatio = iw / ih ; const cRatio = this . cvs . width / this . cvs . height ; let sx , sy , swidth , sheight , dx , dy , dwidth , dheight ; let cropScale ; switch ( bg . type ) { case 'crop' : if ( iRatio > cRatio ) { swidth = ih * cRatio ; sheight = ih ; cropScale = this . cvs . height / ih ; } else { swidth = iw ; sheight = swidth / cRatio ; cropScale = this . cvs . width / iw ; } sx = this . _getBgAlign ( bg . left , iw , this . cvs . width , cropScale ) ; sy = this . _getBgAlign ( bg . top , ih , this . cvs . height , cropScale ) ; dy = dx = 0 ; dheight = this . cvs . height ; dwidth = this . cvs . width ; break ; case 'contain' : sy = sx = 0 ; swidth = iw ; sheight = ih ; if ( iRatio > cRatio ) { dwidth = this . cvs . width ; dheight = dwidth / iRatio ; dx = bg . left || 0 ; dy = ( bg . top || bg . top === 0 ) ? bg . top : ( this . cvs . height - dheight ) / 2 ; } else { dheight = this . cvs . height ; dwidth = dheight * iRatio ; dy = bg . top || 0 ; dx = ( bg . left || bg . left === 0 ) ? bg . left : ( this . cvs . width - dwidth ) / 2 ; } break ; case 'origin' : this . cvs . width = iw ; this . cvs . height = ih ; sx = sy = 0 ; swidth = iw ; sheight = ih ; dx = dy = 0 ; dwidth = this . cvs . width ; dheight = this . cvs . height ; break ; default : throwError ( 'background type error!' ) ; return ; } this . ctx . drawImage ( img , sx , sy , swidth , sheight , dx , dy , dwidth , dheight ) ; this . _next ( ) ; } public rect ( ops = { } ) { this . queue . push ( ( ) => { const { width : cw , height : ch } = this . cvs ; const { fillColor = '#fff' , strokeColor = fillColor , strokeWidth = 0 , radius = 0 , } = ops ; let { width = 100 , height = 100 , x = 0 , y = 0 } = ops ; width = transValue ( cw , 0 , width , 'pos' ) - 2 * strokeWidth , height = transValue ( ch , 0 , height , 'pos' ) - 2 * strokeWidth ; x = transValue ( cw , width , x , 'pos' ) + ( include ( x , 'right' ) ? - strokeWidth : strokeWidth ) ; y = transValue ( ch , height , y , 'pos' ) + ( include ( y , 'bottom' ) ? - strokeWidth : strokeWidth ) ; Canvas . drawRoundRect ( this . ctx , x , y , width , height , radius , fillColor , strokeWidth , strokeColor ) ; this . _resetCtx ( ) . _next ( ) ; } ) ; return this ; } public circle ( ops = { } ) { this . queue . push ( ( ) => { const { fillColor = '#fff' , strokeColor = fillColor , strokeWidth = 0 } = ops ; const { width : cw , height : ch } = this . cvs ; let { x = 0 , y = 0 , radius = 100 } = ops ; const r = transValue ( cw , 0 , radius , 'pos' ) - 2 * strokeWidth ; x = transValue ( cw , 2 * r , x , 'pos' ) + r + ( include ( x , 'right' ) ? - strokeWidth : strokeWidth ) ; y = transValue ( ch , 2 * r , y , 'pos' ) + r + ( include ( y , 'bottom' ) ? - strokeWidth : strokeWidth ) ; this . ctx . beginPath ( ) ; this . ctx . arc ( x , y , r , 0 , Math . PI * 2 , false ) ; this . ctx . fillStyle = fillColor ; this . ctx . fill ( ) ; this . ctx . strokeStyle = strokeColor ; this . ctx . lineWidth = strokeWidth ; this . ctx . stroke ( ) ; this . ctx . closePath ( ) ; this . _resetCtx ( ) . _next ( ) ; } ) ; return this ; } private _resetCtx ( ) { this . ctx . setTransform ( 1 , 0 , 0 , 1 , 0 , 0 ) ; return this ; } public watermark ( image , ops = { } ) { if ( ! image ) { throwError ( 'there is not image of watermark.' ) ; return this ; } const { width = '40%' , pos = 'rightbottom' , margin = 20 } = ops ; const position = { x : 0 , y : 0 , scale : 1 , rotate : 0 , } ; switch ( pos ) { case 'leftTop' : position . x = `left:${ margin }` ; position . y = `top:${ margin }` ; break ; case 'leftBottom' : position . x = `left:${ margin }` ; position . y = `bottom:${ margin }` ; break ; case 'rightTop' : position . x = `right:${ margin }` ; position . y = `top:${ margin }` ; break ; case 'rightBottom' : position . x = `right:${ margin }` ; position . y = `bottom:${ margin }` ; break ; default : } this . add ( image , { width , pos : position , } ) ; return this ; } public add ( image , options ? ) { const def = { width : '100%' , crop : { x : 0 , y : 0 , width : '100%' , height : '100%' , radius : 0 , } , pos : { x : 0 , y : 0 , scale : 1 , rotate : 0 , } , } ; const images = is . arr ( image ) ? image : [ { image , options } ] ; images . map ( ( { image , options } ) => { this . queue . push ( ( ) => { Canvas . getImage ( image ) . then ( img => { this . _add ( img , this . _handleOps ( img , extend ( true , def , options ) ) ) ; } ) . catch ( this . fn . error ) ; } ) ; } ) ; return this ; } private _add ( img , ops ) { const crop = ops . crop as { x : number ; y : number ; width : number ; height : number ; radius : number ; } ; const pos = ops . pos as { x : number ; y : number ; scale : number ; rotate : number ; } ; const width = ops . width as number ; if ( width === 0 ) throwWarn ( `the width of mc-element is zero` ) ; const { iw , ih } = getSize ( img ) ; let cdx , cdy , cdw , cdh ; const { width : lsw , height : lsh , radius } = crop ; if ( lsw !== iw || lsh !== ih || radius > 0 ) { img = cropFn ( img , crop ) . cvs ; } const cratio = lsw / lsh ; let ldx , ldy , ldw , ldh ; const _ratio = iw > ih ? iw / ih : ih / iw ; const lctxScale = _ratio * 1.4 > 5 ? 5 : _ratio * 1.4 ; let spaceX , spaceY ; const [ lcvs , lctx ] = Canvas . create ( Math . round ( lsw * lctxScale ) , Math . round ( lsh * lctxScale ) ) ; const limitLength = belowIOS8 ( ) && ( lcvs . width > 2096 || lcvs . height > 2096 ) ? 2096 : 4096 ; const shrink = cratio > 1 ? limitLength / lcvs . width : limitLength / lcvs . height ; ldx = - Math . round ( lsw / 2 ) ; ldy = - Math . round ( lsh / 2 ) ; ldw = lsw ; ldh = Math . round ( lsw / cratio ) ; if ( ( lcvs . width > limitLength || lcvs . height > limitLength ) && shrink ) { [ lcvs . width , lcvs . height , ldx , ldy , ldw , ldh ] = [ lcvs . width , lcvs . height , ldx , ldy , ldw , ldh ] . map ( v => Math . round ( v * shrink ) ) ; } lctx . translate ( lcvs . width / 2 , lcvs . height / 2 ) ; lctx . rotate ( pos . rotate ) ; lctx . drawImage ( img , ldx , ldy , ldw , ldh ) ; cdw = Math . round ( width * lctxScale ) ; cdh = Math . round ( cdw / cratio ) ; spaceX = ( lctxScale - 1 ) * width / 2 ; spaceY = spaceX / cratio ; cdx = Math . round ( pos . x + cdw * ( 1 - pos . scale ) / 2 - spaceX ) ; cdy = Math . round ( pos . y + cdh * ( 1 - pos . scale ) / 2 - spaceY ) ; cdw *= pos . scale ; cdh *= pos . scale ; this . ctx . drawImage ( lcvs , cdx , cdy , cdw , cdh ) ; this . _next ( ) ; } private _getRotate ( r ? ) { if ( is . str ( r ) ) { return parseFloat ( r ) * Math . PI / 180 ; } else if ( is . num ( r ) ) { return r * Math . PI / 180 ; } else { return 0 ; } } private _handleOps ( img , ops ) { const { width : cw , height : ch } = this . cvs ; const { iw , ih } = getSize ( img ) ; const ratio = iw / ih ; const width = transValue ( cw , iw , ops . width , 'pos' ) ; const cropw = transValue ( cw , iw , ops . crop . width ! , 'crop' ) ; const croph = transValue ( ch , ih , ops . crop . height ! , 'crop' ) ; const crop = { width : cropw , height : croph , x : transValue ( iw , cropw , ops . crop . x ! , 'crop' ) , y : transValue ( ih , croph , ops . crop . y ! , 'crop' ) , radius : getLength ( cropw , ops . crop . radius ! ) , } ; let maxLsw , maxLsh ; if ( crop . x > iw ) crop . x = iw ; if ( crop . y > ih ) crop . y = ih ; maxLsw = iw - crop . x ; maxLsh = ih - crop . y ; if ( crop . width > maxLsw ) crop . width = maxLsw ; if ( crop . height > maxLsh ) crop . height = maxLsh ; const { x : px , y : py , rotate : pr , scale : ps = 1 } = ops . pos ; const pos = { x : transValue ( cw , width , px ! , 'pos' ) , y : transValue ( ch , width / ratio , py ! , 'pos' ) , scale : ps , rotate : this . _getRotate ( pr ) , } ; return { width , crop , pos } ; } private _defaultFontFamily = 'helvetica neue,hiragino sans gb,Microsoft YaHei,arial,tahoma,sans-serif' ; private _createStyle ( fontSize , lineHeight ) { return { font : `${ fontSize }px ${ this . _defaultFontFamily }` , lineHeight , color : '#000' , type : 'fill' , lineWidth : 1 , wordBreak : true , shadow : { color : null , blur : 0 , offsetX : 0 , offsetY : 0 , } , } ; } public text ( context , ops = { } ) { const dfs = this . cvs . width / 20 ; this . queue . push ( ( ) => { const option = extend ( true , { width : 300 , align : 'left' , smallStyle : this . _createStyle ( dfs * 0.8 , dfs * 0.9 ) , normalStyle : this . _createStyle ( dfs , dfs * 1.1 ) , largeStyle : this . _createStyle ( dfs * 1.3 , dfs * 1.4 ) , pos : { x : 0 , y : 0 , rotate : 0 , } , } , ops ) as Required < TCanvas . textOptions > ; const parseContext = this . _parse ( String ( context ) ) ; let max = 0 , maxFont ; parseContext . map ( v => { if ( v . size > max ) { max = v . size ; maxFont = v . type ; } } ) ; const maxFontSize = parseInt ( option [ `${ maxFont }Style` ] . font ) ; if ( maxFontSize && option . width < maxFontSize ) option . width = maxFontSize ; this . _text ( parseContext , option ) ; this . _resetCtx ( ) . _next ( ) ; } ) ; return this ; } private _parse ( context ) { const arr = context . split ( /<s>|<b>/ ) ; const result = [ ] ; for ( let i = 0 ; i < arr . length ; i ++ ) { const value = arr [ i ] ; if ( /<\/s>|<\/b>/ . test ( value ) ) { const splitTag = /<\/s>/ . test ( value ) ? '</s>' : '</b>' , type = /<\/s>/ . test ( value ) ? 'small' : 'large' , tmp = arr [ i ] . split ( splitTag ) ; result . push ( { type , text : tmp [ 0 ] , size : type === 'small' ? 0 : 2 , } ) ; tmp [ 1 ] && result . push ( { type : 'normal' , text : tmp [ 1 ] , size : 1 , } ) ; continue ; } arr [ i ] && result . push ( { text : arr [ i ] , type : 'normal' , size : 1 , } ) ; } return result ; } private _text ( textArr , option ) { this . data . textId ++ ; this . data . text [ this . data . textId ] = { } ; const opsWidth = option . width = transValue ( this . cvs . width , 0 , option . width , 'pos' ) ; let style , line = 1 , lineWidth = 0 , lineHeight = this . _getLineHeight ( textArr , option ) , x = transValue ( this . cvs . width , opsWidth , 0 , 'pos' ) , y = ( transValue ( this . cvs . height , 0 , 0 , 'pos' ) ) + lineHeight ; this . data . text [ this . data . textId ] [ line ] = { data : [ ] , lineWidth : 0 , } ; textArr . map ( v => { style = option [ `${ v . type }Style` ] ; this . ctx . font = style . font ; let width = this . ctx . measureText ( v . text ) . width ; let context = v . text . replace ( /<br>/g , '|' ) ; if ( ( lineWidth + width ) > opsWidth || include ( context , '|' ) ) { if ( ! style . wordBreak ) context = splitWords ( context ) ; for ( let i = 0 , fontLength = context . length ; i < fontLength ; i ++ ) { const _context = context [ i ] ; width = this . ctx . measureText ( _context ) . width ; if ( ( lineWidth + width ) > opsWidth || _context === '|' ) { x = lineWidth = 0 ; y += lineHeight ; line += 1 ; this . data . text [ this . data . textId ] [ line ] = { data : [ ] , lineWidth : 0 , } ; if ( _context === '|' ) continue ; } const lineData = this . data . text [ this . data . textId ] [ line ] ; lineData . data . push ( { context : _context , x , y , style , width } ) ; x += width ; lineData . lineWidth = lineWidth += width ; } } else { const lineData = this . data . text [ this . data . textId ] [ line ] ; lineData . data . push ( { context , x , y , style , width } ) ; x += width ; lineData . lineWidth = lineWidth += width ; } } ) ; const [ tcvs , tctx ] = Canvas . create ( opsWidth , this . _getTextRectHeight ( line ) ) ; const tdh = tcvs . height ; const tdw = tcvs . width ; const tdx = transValue ( this . cvs . width , tdw , option . pos . x ! , 'pos' ) ; const tdy = transValue ( this . cvs . height , tdh , option . pos . y ! , 'pos' ) ; forin ( this . data . text [ this . data . textId ] , ( k , v ) => { let add = 0 ; if ( v . lineWidth < opsWidth ) { if ( option . align === 'center' ) { add = ( opsWidth - v . lineWidth ) / 2 ; } else if ( option . align === 'right' ) { add = opsWidth - v . lineWidth ; } } v . data . map ( text => { text . x += add ; this . _fillText ( tctx , text ) ; } ) ; } ) ; this . ctx . translate ( tdx + tdw / 2 , tdy + tdh / 2 ) ; this . ctx . rotate ( this . _getRotate ( option . pos . rotate ) ) ; this . ctx . drawImage ( tcvs , - tdw / 2 , - tdh / 2 , tdw , tdh ) ; } private _getLineHeight ( textArr , option ) { let lh = 0 , vlh ; textArr . map ( v => { vlh = option [ `${ v . type }Style` ] . lineHeight ; if ( vlh > lh ) lh = vlh ; } ) ; return lh ; } private _fillText ( ctx , text ) { const { context , x , y , style } = text ; const { align , lineWidth , shadow , font , gradient , lineHeight } = style ; const { color , blur , offsetX , offsetY } = shadow ; ctx . font = font ; ctx . textAlign = align ; ctx . textBaseline = 'alphabetic' ; ctx . lineWidth = lineWidth ; ctx . shadowColor = color ; ctx . shadowBlur = blur ; ctx . shadowOffsetX = offsetX ; ctx . shadowOffsetY = offsetY ; if ( gradient ) { const { type , colorStop } = gradient ; let x1 , y1 , x2 , y2 ; if ( type === 1 ) { x1 = x ; y1 = y ; x2 = x + text . width ; y2 = y ; } else { x1 = x ; y1 = y - lineHeight ; x2 = x ; y2 = y ; } const grad = ctx . createLinearGradient ( x1 , y1 , x2 , y2 ) ; const colorNum = colorStop . length || 0 ; forin ( colorStop , ( i , v ) => { grad . addColorStop ( 1 / colorNum * ( + i + 1 ) , v ) ; } ) ; ctx [ `${ style . type }Style` ] = grad ; } else { ctx [ `${ style . type }Style` ] = style . color ; } ctx [ `${ style . type }Text` ] ( context , x , y ) ; this . _resetCtx ( ) ; } private _getTextRectHeight ( lastLine ) { const lastLineData = this . data . text [ this . data . textId ] [ lastLine ] . data [ 0 ] ; return lastLineData . y + lastLineData . style . lineHeight ; } public draw ( ops = { } ) { return new Promise ( ( resolve , reject ) => { let config = { type : 'jpeg' , quality : .9 , exportType : 'base64' , success ( b64 ) { } , error ( err ) { } , } ; if ( is . fn ( ops ) ) { config . success = ops ; } else { config = extend ( true , config , ops ) ; if ( config . type === 'jpg' ) config . type = 'jpeg' ; } this . fn . error = ( err ) => { config . error ( err ) ; reject ( err ) ; } ; this . fn . success = ( ) => { if ( config . exportType === 'canvas' ) { config . success ( this . cvs ) ; resolve ( this . cvs ) ; } else { setTimeout ( ( ) => { const b64 = this . cvs . toDataURL ( `image/${ config . type }` , config . quality ) ; config . success ( b64 ) ; resolve ( b64 ) ; } , 0 ) ; } } ; this . _next ( ) ; } ) ; } private _next ( ) { if ( this . queue . length > 0 ) { this . ctx . save ( ) ; const next = this . queue . shift ( ) ; next && next ( ) ; this . ctx . restore ( ) ; } else { this . fn . success ( ) ; } } public clear ( ) { this . ctx . clearRect ( 0 , 0 , this . cvs . width , this . cvs . height ) ; return this ; } }