import { MiBrushAttrs , MiBrushRepaintConfig , MiTools , MiToolsSyncData , Tools } from '@components/canvas/Tools' ; import { Canvas } from '@components/canvas/Canvas' ; import { Point } from '@components/canvas/Point' ; import { MiRectConfig } from '@components/canvas/shapes/Rect' ; import { MiLayerData } from '@components/canvas/Layer' ; import { Utils } from '@components/canvas/Utils' ; export interface MiTextConfig { size : number ; content : string | null ; point : Point ; attrs : MiBrushAttrs ; rect : MiRectConfig ; index ? : number | null ; visible ? : boolean ; max : { width : number ; height : number ; } ; } export class Text extends Tools implements MiTools { name = 'text' ; divisor = 2 ; static data ? : MiTextConfig ; static instance : Text ; static wid = '' ; layerData ? : MiLayerData ; protected start ! : Point ; protected content = '' ; /** 最大宽高(用于自动换行/控制滚动条) */ protected max : { width : number ; height : number ; } = { width : 0 , height : 0 } ; /** 文本内容所占的方形区域 */ protected rect : MiRectConfig = { x : 0 , y : 0 , width : 0 , height : 0 } ; protected cid = '' ; protected tid = '' ; protected pid = '' ; param canvas constructor ( canvas ) { super ( canvas ) ; Text . instance = this ; Tools . instances [ this . name ] = this ; } param args draw < A extends MiToolsSyncData [ ] > ( ... args ) { const data = args [ 0 ] as any , text = Utils . deepCopy ( data . text ) as MiTextConfig , attrs = text . attrs as MiBrushAttrs ; Text . rendering ( { size : this . getThickness ( this . divisor , attrs . thickness ) , content : text . content , point : new Point ( text . point . x , text . point . y ) , attrs , rect : Utils . deepCopy ( text . rect ) , max : Utils . deepCopy ( text . max ) } , false , this , data . config ) ; } param data param config param ctx protected drawText ( data , config ? , ctx ? ) { const content = data . content ; if ( content ) { ctx = ctx ?? this . getCurrentContext ( ) ; const attrs = data . attrs , size = this . getThickness ( this . divisor , attrs . thickness ) , breakContent = content . split ( '\n' ) , scale = config && config . scale ; ctx . save ( ) ; /** 设置属性 */ ctx . font = `${ size }px Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, 微软雅黑, Arial, sans-serif` ; ctx . textBaseline = 'top' ; ctx . fillStyle = Utils . colorHexToRgba ( attrs . color , attrs . opacity ) ; /** 细微的偏移量调整 */ const offset = size >= 25 ? ( size >= 34 ? 4 : 5 ) : 6 ; let x = data . point . x - 2 , y = data . point . y - offset ; for ( let i = 0 , len = breakContent . length ; i < len ; i ++ ) { const charContent = breakContent [ i ] . split ( '' ) ; let lineFeedStr = '' , spacing = 0 ; for ( let k = 0 , length = charContent . length ; k < length ; k ++ ) { const letter = charContent [ k ] , letterWidth = ctx . measureText ( letter ) . width ; lineFeedStr += letter ; spacing ++ ; /** 缩放后, 且在设定最大宽高时, 分别 +21/-82 了 */ const lineFeedStrWidth = ctx . measureText ( lineFeedStr ) . width * ( scale ?? 1 ) + ( spacing * 2 * ( scale ?? 1 ) ) + 21 ; if ( k > 0 && lineFeedStrWidth > ( data . max . width ) ) { /** 自动换行绘制 */ x = data . point . x - 2 ; y += size + 10 ; lineFeedStr = letter ; spacing = 0 ; ctx . fillText ( letter , x , y ) ; x += letterWidth + 2 ; } else { /** 正常绘制 */ ctx . fillText ( letter , x , y ) ; x += letterWidth + 2 ; } } /** 手动换行, 更新坐标 */ x = data . point . x - 2 ; y += size + 10 ; } ctx . restore ( ) ; } } param data param external param instance param config param ctx static rendering ( data , external , instance , config ? , ctx ? ) { const content = data . content ?? null ; if ( content ) { ctx = ctx ?? config ?. ctx ?? Text . prototype . getCurrentContext ( ) ; const tempData = Utils . deepCopy ( data ) , rectangle = tempData . rect as MiRectConfig ; ctx . save ( ) ; if ( config ) { /** 无限画布 */ if ( config . origin ) ctx . translate ( config . origin . x , config . origin . y ) ; /** 移动 */ if ( config . move ) { const isMoving = config . move . x !== 0 || config . move . y !== 0 ; if ( isMoving ) { tempData . start . x += config . move . x ; tempData . start . y += config . move . y ; rectangle . x += config . move . x ; rectangle . y += config . move . y ; } } } if ( config ?. selection ) instance . drawChoiceRect ( ctx , rectangle ) ; else instance . drawText ( tempData , config , ctx ) ; ctx . restore ( ) ; if ( external ) instance . saveData ( ) ; } } param point protected createWrapper ( point ) { /** 清除上一次生成的文本域 */ const previous = document . getElementById ( Text . wid ) ; if ( previous ) previous . remove ( ) ; /** 分别是 wrapper id / pre id / content id / textarea id */ Text . wid = Utils . uid ( ) ; this . pid = Utils . uid ( ) ; this . cid = Utils . uid ( ) ; this . tid = Utils . uid ( ) ; /** 创建 div, 绝对定位 */ const wrapper = document . createElement ( 'div' ) , scroll = this . createScroll ( point ) ; let left = point . x - 4 , top = point . y - 12 ; if ( this . layerData ) { const scale = this . layerData . scale ; if ( scale ) { /**
                 * 原方形比内容区域的宽高各多出10, 缩放后这部分也需要变更.
                 * 原内容区域左右有 padding 2px, 这部分也不能忘(上下没有).
                 */ left += 6 * ( scale - 1 ) ; top += scale - 1 ; } } /** 偏移量 */ const offset = this . canvas . getCanvas ( ) . getBoundingClientRect ( ) ; left += offset . left ; top += offset . top ; /** 移动 */ if ( this . layerData ) { const move = this . layerData . move ; left += move . x ; top += move . y ; } wrapper . id = Text . wid ; wrapper . className = 'mi-canvas-text-wrapper' ; wrapper . style . left = `${ left }px` ; wrapper . style . top = `${ top }px` ; wrapper . appendChild ( scroll ) ; document . body . append ( wrapper ) ; /** textarea 延迟聚焦 */ setTimeout ( ( ) => { const textarea = document . getElementById ( this . tid ) ; if ( textarea ) textarea . focus ( ) ; this . adaptTextarea ( ) ; } , 50 ) ; } param point protected createScroll ( point ) { const scroll = document . createElement ( 'div' ) , pre = this . createPre ( ) , textarea = this . createTextarea ( point ) ; scroll . className = 'mi-canvas-text-scroll' ; scroll . appendChild ( pre ) ; scroll . appendChild ( textarea ) ; return scroll ; } return { HTMLPreElement } protected createPre ( ) { const pre = document . createElement ( 'pre' ) , content = document . createElement ( 'span' ) , br = document . createElement ( 'br' ) ; pre . id = this . pid ; content . id = this . cid ; pre . appendChild ( content ) ; pre . appendChild ( br ) ; this . setContentSize ( pre ) ; return pre ; } param point protected createTextarea ( point ) { const textarea = document . createElement ( 'textarea' ) ; textarea . id = this . tid ; this . setContentColor ( textarea ) ; this . setContentSize ( textarea ) ; this . setContentMaxArea ( textarea , point ) ; return textarea ; } return protected adaptTextarea ( ) { const area = document . getElementById ( this . tid ) as HTMLTextAreaElement , content = document . getElementById ( this . cid ) ; if ( area && content ) { /** 输入 */ Utils . on ( area , 'input' , ( ) => { content . textContent = area . value ; this . content = area . value ; this . setContentRect ( content ) ; this . setData ( area . value ) ; } ) ; /** 删除 */ Utils . on ( area , 'keyup' , ( evt ) => { const key = evt as KeyboardEvent , code = key . keyCode ; if ( code === 8 || code === 46 ) { const str = area . value . trim ( ) ; content . textContent = str ; this . setContentRect ( content ) ; this . setData ( str ) ; } } ) ; /** 复制粘贴 */ Utils . on ( area , 'cut' , ( ) => { content . textContent = area . value ; this . setContentRect ( content ) ; this . setData ( area . value ) ; } ) ; /** 初始化 */ if ( this . layerData ) { const text = this . getTextContent ( ) as MiTextConfig ; content . textContent = text . content ; area . value = ( text . content as string ) . toString ( ) ; } else content . textContent = area . value ; } } param content protected setContentRect ( content ? ) { content = content ?? document . getElementById ( this . cid ) as HTMLSpanElement ; this . rect . width = Math . ceil ( content . offsetWidth ) + 10 ; this . rect . height = Math . ceil ( content . offsetHeight ) + 10 ; } param textarea param point protected setContentMaxArea ( textarea , point ) { const max = { width : this . canvas . width - point . x + 21 , height : this . canvas . height - point . y - 82 } ; textarea . style . maxWidth = `${ max . width }px` ; textarea . style . maxHeight = `${ max . height }px` ; this . max = max ; } param elem param size param setRect protected setContentSize ( elem , size ? , setRect ? ) { const text = this . getTextContent ( ) ; let nSize = size ?? this . getThickness ( this . divisor ) ; if ( text && ! size ) nSize = text . size ; elem . style . fontSize = `${ nSize }px` ; elem . style . lineHeight = `${ nSize + 10 }px` ; elem . style . letterSpacing = '2px' ; if ( setRect ) this . setContentRect ( elem ) ; } param thickness setContentSizeAfterChange ( thickness ) { if ( Text . data && Text . data . attrs . thickness === thickness ) return ; const content = document . getElementById ( this . cid ) ; if ( content ) { const textarea = document . getElementById ( this . tid ) , size = this . getThickness ( this . divisor , thickness ) ; if ( textarea ) { if ( Text . data ) Text . data . attrs . thickness = thickness ; this . setContentSize ( textarea , size ) ; } const pre = document . getElementById ( this . pid ) ; if ( pre ) { this . setContentSize ( pre , size ) ; this . setContentRect ( content ) ; } } } param elem param color param opacity protected setContentColor ( elem , color ? , opacity ? ) { const text = this . getTextContent ( ) ; let nColor = color ?? Tools . color , nOpacity = opacity !== undefined && parseInt ( opacity as string ) >= 0 ? opacity . toString ( ) : Tools . opacity . toString ( ) ; if ( text && ! color ) nColor = text . attrs . color ; if ( text && opacity === undefined ) nOpacity = text . attrs . opacity . toString ( ) ; elem . style . opacity = nOpacity ; elem . style . color = nColor ; } param color param opacity setContentColorAfterChange ( color ? , opacity ? ) { const content = document . getElementById ( this . cid ) ; if ( content ) { const textarea = document . getElementById ( this . tid ) ; if ( textarea ) { if ( Text . data && color ) Text . data . attrs . color = color ; if ( Text . data && opacity !== undefined ) Text . data . attrs . opacity = opacity ; this . setContentColor ( textarea , color , opacity ) ; } } } param index param attrs param rect param oAttrs param oRect protected saveTrace ( index ? , attrs ? , rect ? , oAttrs ? , oRect ? ) { if ( index !== undefined ) { const params = { operation : 'edit' , index , active : { } , old : { } } as any ; if ( attrs ) params . active . attrs = attrs ; if ( rect ) params . active . rect = rect ; if ( oAttrs ) params . old . attrs = oAttrs ; if ( oRect ) params . old . rect = oRect ; Tools . setTraces ( params ) ; } else Tools . setTraces ( ) ; } param content protected setData ( content ) { if ( this . layerData ) { /** 编辑状态 */ const data = Text . data as MiTextConfig ; data . rect = { ... this . rect } ; data . content = content ? content . trim ( ) : '' ; } else { Text . data = Utils . deepCopy ( { size : this . getThickness ( this . divisor ) , content : content ? content . trim ( ) : '' , point : this . start , attrs : { ... this . getCtxAttrs ( ) } , rect : this . rect , max : this . max } ) ; } } param data param layerData protected isEditing ( data ? , layerData ? ) { let result = false ; data = ( data ?? Text . data ) as MiTextConfig ; layerData = layerData ?? this . layerData ; if ( layerData ) { const text = layerData . text as MiTextConfig ; if ( text . content !== data . content || text . attrs . color !== data . attrs . color || text . attrs . opacity !== data . attrs . opacity || text . attrs . thickness !== data . attrs . thickness ) result = true ; } return result ; } return protected saveData ( data ? , layerData ? , index ? ) { data = data ?? Utils . deepCopy ( Text . data ) as MiTextConfig ; layerData = layerData ?? this . layerData ; index = index !== undefined ? index : Tools . getLayer ( ) . index ; if ( layerData && index !== undefined && index !== null ) { /** 编辑 */ if ( this . isEditing ( data , layerData ) ) { const text = Utils . deepCopy ( layerData . text ) as MiTextConfig ; this . saveTrace ( index , data . attrs , data . rect , text . attrs , text . rect ) ; layerData . rect = { ... data . rect } ; layerData . text = data ; layerData . draw = ( config ) => { Text . rendering ( data as MiTextConfig , false , this , config ) ; } ; } } else { /** 常规 */ const params = { ... this . getDefaultSyncData ( ) , rect : { ... data . rect } , text : data , attrs : { ... data . attrs } , tool : this . name } as MiToolsSyncData ; this . saveLayerData ( params ) ; this . saveTrace ( ) ; } Text . data = undefined ; this . layerData = undefined ; Tools . getLayer ( ) . index = null ; } return protected getTextContent ( ) { let text = null ; if ( this . layerData ) text = this . layerData . text as MiTextConfig ; return text ; } param point protected isPointInTextarea ( point ) { const data = Tools . getLayer ( ) . data , len = data . length ; let isIn = false ; for ( let i = len - 1 ; i >= 0 ; i -- ) { const cur = data [ i ] ; if ( cur . visible && cur . tool === 'text' && cur . rect ) { if ( this . isPointInRect ( point , cur . rect , cur . move , cur . scale , cur . origin ) ) { isIn = true ; this . layerData = cur ; Tools . getLayer ( ) . index = i ; break ; } } } return isIn ; } param event protected drawBegin ( event ) { let point = this . createPoint ( event . clientX , event . clientY ) ; if ( Text . data ) { if ( Text . data . content ) { /**
                 * 上一次(未渲染)
                 * 离开文本工具 / 文本输入的起始位置变更
                 * 需先保存上次文本域内的文本内容(绘制)
                 * 进而清理 Text.data, 再保存数据.
                 */ Text . rendering ( Text . data , false , this , this . layerData ? { scale : this . layerData . scale , move : { ... this . layerData . move } , origin : { ... this . layerData . origin } , rect : Utils . deepCopy ( this . layerData . rect ) } : undefined ) ; this . saveData ( ) ; } else { /** 编辑状态下, 清除保存的数据 */ if ( this . layerData ) Tools . getLayer ( ) . clear ( Tools . getLayer ( ) . index as number ) ; /** 恢复 */ Text . data = undefined ; this . layerData = undefined ; Tools . getLayer ( ) . index = null ; } } if ( this . isPointInTextarea ( point ) ) { /** 重绘(不包含当前选中) */ Tools . getStage ( ) . draw ( ) ; /** 初始化选中内容 */ if ( this . layerData ) { /** 封装内容 */ const data = Utils . deepCopy ( this . layerData . text ) as MiTextConfig ; /** 设置 rect, 避免保存时为初始状态值 */ this . rect = data . rect ; /** 编辑的文本索引 */ data . index = Tools . getLayer ( ) . index ; const start = new Point ( data . point . x , data . point . y ) ; this . start = start ; point = start ; Text . data = data ; } } else { /**
             * 1. 首次.
             * 2. 上一次操作非 "编辑" 状态.
             * 3. 未选中已存在的文本内容.
             */ this . start = point ; this . rect . x = point . x - 8 ; this . rect . y = point . y - 13 ; } this . createWrapper ( point ) ; } param event protected drawUpdate ( event ) { } param event protected drawEnd ( event ) { } }