import { Canvas } from '@components/canvas/Canvas' ; import { Utils } from '@components/canvas/Utils' ; import { MiStageTracesConfig , Stage } from '@components/canvas/Stage' ; import { Layer , MiLayerData } from '@components/canvas/Layer' ; import { Events } from '@components/canvas/Events' ; import { throttle } from '@components/canvas/Throttle' ; import { MiPoint , MiPointConfig , MiPointGroup , Point } from '@components/canvas/Point' ; import { MiRectConfig , MiRectPointConfig } from '@components/canvas/shapes/Rect' ; import RectWorker from 'worker-loader!./workers/Rect.worker' ; import { cookie } from '@utils/cookie' ; import { Rect } from '@components/canvas/shapes/Rect' ; import { MiArcConfig } from '@components/canvas/shapes/Arc' ; import { MiTextConfig } from '@components/canvas/tools/Text' ; export interface MiTools { param args draw ( ... args ) : void ; } /** 笔刷属性 */ export interface MiBrushAttrs { color : string ; thickness : number ; opacity : number ; } /** 可配置的笔刷属性 */ export interface MiBrushAttrsConfig { name ? : string ; color ? : string ; thickness ? : number ; opacity ? : number ; solid ? : boolean ; type ? : string ; } /** 重绘属性 */ export interface MiBrushRepaintConfig { ctx ? : CanvasRenderingContext2D ; move ? : MiPointConfig ; origin ? : MiPointConfig ; scale ? : number ; rotate ? : number ; rect ? : MiRectConfig ; text ? : string ; selection ? : boolean ; offScreen ? : boolean ; } /** 笔刷数据 */ export interface MiBrushData { name : string ; icon : string ; tool : string ; solid ? : boolean ; } /** 同步 / 存储数据 */ export interface MiToolsSyncData { cid : number ; lid : number ; uid : number ; tool : string ; points ? : Point [ ] ; attrs ? : MiBrushAttrs ; start ? : Point ; end ? : Point ; solid ? : boolean ; rect ? : MiRectConfig ; config ? : MiBrushRepaintConfig ; text ? : MiTextConfig ; } export abstract class Tools extends Events { id : string ; abstract name : string ; static color = '#000000' ; static thickness = 20 ; static opacity = .8 ; static distance = 5 ; static throttle = 16 ; repeat = false ; offset = 10 ; enter = false ; toolbar = true ; protected static stage : Stage ; protected static layer : Layer ; protected drawing = false ; protected canvas : Canvas ; protected buffer ! : Canvas ; protected ctx : CanvasRenderingContext2D ; protected static instances : { [ index ] : any ; } = { } ; protected static buffers : { [ index ] : any ; } = { } ; protected drawProcess : ( event : MouseEvent | PointerEvent | Touch ) => void ; param canvas protected constructor ( canvas ) { super ( ) ; this . id = Utils . uid ( ) ; this . canvas = canvas ; this . ctx = canvas . getContext ( ) ; this . ctx . lineJoin = this . ctx . lineCap = 'round' ; const color = Utils . colorHexToRgba ( Tools . color , Tools . opacity ) ; this . ctx . strokeStyle = color ; this . ctx . fillStyle = color ; this . drawProcess = this . drawUpdate ; if ( Tools . throttle ) this . drawProcess = throttle ( this . drawUpdate , Tools . throttle ) ; } return static getInstances ( ) { return Tools . instances ; } return { Stage } static getStage ( ) { return Tools . stage ; } param stage static setStage ( stage ) { Tools . stage = stage ; } return { Layer } static getLayer ( ) { return Tools . layer ; } param layer static setLayer ( layer ) { Tools . layer = layer ; } param attrs static setAttrs ( attrs ) { for ( const i in attrs ) { if ( Object . prototype . hasOwnProperty . call ( attrs , i ) ) { ( Tools as any ) [ i ] = ( attrs as any ) [ i ] ; } } } return static backward ( ) { const stage = Tools . getStage ( ) , layer = Tools . getLayer ( ) , data = layer . data , traces = stage . traces , trace = traces . length > 0 ? traces . pop ( ) : null ; const recovery = ( ) => { stage . recoveries . push ( trace ) ; stage . draw ( true ) ; } ; if ( trace ) { if ( trace . operation === 'clean' ) { /** 清屏 */ const indexes = trace . indexes ; for ( let i = 0 , len = data . length ; i < len ; i ++ ) { if ( indexes . includes ( i ) ) data [ i ] . visible = true ; } recovery ( ) ; } else if ( trace . index !== undefined && data [ trace . index ] ) { switch ( trace . operation ) { case 'draw' : data [ trace . index ] . visible = false ; break ; case 'move' : data [ trace . index ] . move . x -= trace . move . x ; data [ trace . index ] . move . y -= trace . move . y ; break ; case 'edit' : const text = data [ trace . index ] . text as MiTextConfig ; text . attrs = trace . old . attrs ; text . rect = trace . old . rect ; break ; case 'eraser' : data [ trace . index ] . visible = true ; break ; } recovery ( ) ; } } } return static forward ( ) { const stage = Tools . getStage ( ) , data = Tools . getLayer ( ) . data , recoveries = stage . recoveries , recovery = recoveries . length > 0 ? recoveries . pop ( ) : null ; const trace = ( ) => stage . traces . push ( recovery ) ; if ( recovery ) { if ( recovery . operation === 'clean' ) { /** 清屏恢复 */ for ( let i = 0 , len = data . length ; i < len ; i ++ ) { const indexes = recovery . indexes ; if ( indexes . includes ( i ) ) data [ i ] . visible = false ; } trace ( ) ; } else if ( recovery . index !== undefined && data [ recovery . index ] ) { switch ( recovery . operation ) { case 'draw' : data [ recovery . index ] . visible = true ; break ; case 'move' : data [ recovery . index ] . move . x += recovery . move . x ; data [ recovery . index ] . move . y += recovery . move . y ; break ; case 'edit' : const text = data [ recovery . index ] . text as MiTextConfig ; text . attrs = recovery . active . attrs ; text . rect = recovery . active . rect ; break ; case 'eraser' : data [ recovery . index ] . visible = false ; break ; } trace ( ) ; } stage . draw ( true ) ; } } param params static setTraces ( params ? ) { params = params ?? { } ; if ( params . operation === undefined ) params . operation = 'draw' ; if ( params . index === undefined && params . indexes === undefined ) params . index = Tools . getLayer ( ) . data . length - 1 ; Tools . getStage ( ) . traces . push ( params ) ; } param points param move protected moving ( points , move ) { for ( let i = 0 , len = points . length ; i < len ; i ++ ) { points [ i ] . x += move . x ; points [ i ] . y += move . y ; } return points ; } param point param rect param move param scale param origin param action param ctx protected isPointInRect ( point , rect , move ? , scale ? , origin ? , action ? , ctx ? ) { const tempPoint = { ... point } , tempRect = { ... rect } ; ctx = ctx ?? this . getCurrentContext ( ) ; let isIn = false ; ctx . save ( ) ; ctx . beginPath ( ) ; /** 移动 */ if ( move ) { tempRect . x += move . x ; tempRect . y += move . y ; } /** 缩放 */ if ( ! action && scale && scale !== 1 ) { /** 按倍数缩放方形宽高 */ tempRect . width *= scale ; tempRect . height *= scale ; } /** 无限画布 */ if ( origin ) ctx . translate ( origin . x , origin . y ) ; ctx . rect ( tempRect . x , tempRect . y , tempRect . width , tempRect . height ) ; isIn = ctx . isPointInPath ( tempPoint . x , tempPoint . y ) ; ctx . closePath ( ) ; ctx . restore ( ) ; return isIn ; } param ctx param rect param config protected drawChoiceRect ( ctx , rect , config ? ) { if ( config ?. rect ) { if ( config . selection ) this . drawSelectionRect ( { ... rect } , ctx ) ; } } param rect param ctx protected drawSelectionRect ( rect , ctx ? ) { ctx = ctx ?? this . getCurrentContext ( ) ; const color = 'rgb(47, 150, 136)' ; ctx . lineWidth = 3 ; ctx . lineDashOffset = 3 ; ctx . setLineDash ( [ 8 ] ) ; ctx . strokeStyle = color ; new Rect ( { ... rect , radius : 4 } ) . draw ( ctx ) ; } param rect param move param scale param origin param ctx protected drawEraserRect ( rect , move ? , scale ? , origin ? , ctx ? ) { const color = 'rgb(116, 116, 116)' , rectangle = { ... rect } ; rectangle . radius = 4 ; ctx = ctx ?? this . getCurrentContext ( ) ; ctx . save ( ) ; ctx . lineDashOffset = 3 ; ctx . setLineDash ( [ 8 ] ) ; ctx . strokeStyle = color ; /** 移动 */ if ( move ) { rectangle . x += move . x ; rectangle . y += move . y ; } /** 无限画布 */ if ( origin ) ctx . translate ( origin . x , origin . y ) ; /** 缩放 */ if ( scale && scale !== 1 ) { rectangle . width *= scale ; rectangle . height *= scale ; } /** 绘制 */ new Rect ( { ... rectangle } ) . draw ( ctx ) ; ctx . restore ( ) ; } param points param callback protected drawRectWorker ( points , callback ? ) { const worker = new RectWorker ( ) ; worker . postMessage ( points ) ; worker . onmessage = ( data ) => { const rect = data . data as MiRectConfig ; if ( typeof callback === 'function' ) callback . call ( this , rect ) ; } ; } param color param opacity param ctx protected setCtxAttrs ( color ? , opacity ? , ctx ? ) { ctx = ctx ?? this . getCurrentContext ( ) ; color = Utils . colorHexToRgba ( color ?? Tools . color , opacity ?? Tools . opacity ) ; ctx . strokeStyle = color ; ctx . fillStyle = color ; } param ctx param attrs param divisor protected resetCtxAttrs ( ctx ? , attrs ? , divisor = 10 ) { ctx = ctx ?? this . getCurrentContext ( ) ; /** 重设属性, 避免冲突(共享属性) */ attrs = attrs ?? this . getCtxAttrs ( ) ; ctx . lineWidth = this . getThickness ( divisor , attrs . thickness ) ; this . setCtxAttrs ( attrs . color , attrs . opacity , ctx ) ; } param divisor param thickness protected getThickness ( divisor ? , thickness ? ) { return Math . ceil ( ( thickness ?? Tools . thickness ) / ( divisor ?? 10 ) ) ; } return { MiBrushAttrs } protected getCtxAttrs ( ) { return { color : Tools . color , thickness : Tools . thickness , opacity : Tools . opacity } ; } param rect protected getRect ( rect ) { return { x : rect . x - this . offset , y : rect . y - this . offset , width : rect . width + ( this . offset * 2 ) , height : rect . height + ( this . offset * 2 ) } ; } param arc protected getCircleRect ( arc ) { return this . getRect ( { x : Math . ceil ( arc . x - arc . radius ) , y : Math . ceil ( arc . y - arc . radius ) , width : Math . ceil ( arc . radius * 2 ) , height : Math . ceil ( arc . radius * 2 ) } ) ; } param clear protected getBufferContext ( clear = true ) { const ctx = this . buffer . getContext ( ) ; if ( clear ) this . buffer . clear ( ) ; return ctx ; } return protected getCurrentContext ( ) { return Tools . getStage ( ) . canvas . getContext ( ) ; } param data protected getLastPoints ( data ) { const pointGroup = data [ data . length - 1 ] ; return pointGroup . points ?? [ ] ; } param p1 param p2 protected getMidPoint ( p1 , p2 ) { return { x : p1 . x + ( p2 . x - p1 . x ) / 2 , y : p1 . y + ( p2 . y - p1 . y ) / 2 } ; } param start param end param rect protected getPointsBasedOn2Point ( start , end , rect ? ) { rect = rect ?? { x : 0 , y : 0 , width : 0 , height : 0 } ; /** 右上角移动 */ if ( end . x > start . x && end . y < start . y ) { rect . x = start . x ; rect . y = end . y ; rect . width = end . x - start . x ; rect . height = start . y - end . y ; } /** 右下角移动 */ if ( end . x > start . x && end . y > start . y ) { rect . x = start . x ; rect . y = start . y ; rect . width = end . x - start . x ; rect . height = end . y - start . y ; } /** 左上角移动 */ if ( end . x < start . x && end . y < start . y ) { rect . x = end . x ; rect . y = end . y ; rect . width = start . x - end . x ; rect . height = start . y - end . y ; } /** 左下角移动 */ if ( end . x < start . x && end . y > start . y ) { rect . x = end . x ; rect . y = start . y ; rect . width = start . x - end . x ; rect . height = end . y - start . y ; } const p1 = { x : rect . x , y : rect . y } , p2 = { x : p1 . x + rect . width , y : p1 . y } , p3 = { x : p1 . x + rect . width , y : p1 . y + rect . height } , p4 = { x : p1 . x , y : p1 . y + rect . height } ; return { lt : p1 , rt : p2 , rb : p3 , lb : p4 } ; } /**
     * 默认数据
     */ protected getDefaultSyncData ( ) { return { cid : Tools . stage ? Tools . stage . index : 1 , lid : Tools . layer ? Tools . layer . idx : 1 , uid : cookie . get ( 'uid' ) || 1 , } as MiToolsSyncData ; } return protected getDefaultLayerData ( ) { return { move : { x : 0 , y : 0 } , scale : 1 , rotate : 0 , origin : { x : 0 , y : 0 } , visible : true } ; } param params protected saveLayerData ( params ) { const data = { ... this . getDefaultLayerData ( ) , tool : params . tool , rect : params . rect , draw : ( config ) => { params . config = config ; const tool = Tools . instances [ params . tool ] ; if ( tool ) tool . draw ( params ) ; } } as MiLayerData ; if ( params . text ) data . text = params . text ; Tools . getLayer ( ) . data . push ( data ) ; } param line param color protected createBuffer ( line ? , color ? ) { const buffer = this . canvas . create ( ) , ctx = buffer . getContext ( ) ; color = color ?? Utils . colorHexToRgba ( Tools . color , Tools . opacity ) ; ctx . lineCap = ctx . lineJoin = 'round' ; ctx . lineWidth = line ?? ( Tools . thickness / 10 ) ; ctx . strokeStyle = color ; ctx . fillStyle = color ; return buffer ; } param callback protected resetBuffer ( callback ? ) { if ( this . buffer && Object . keys ( this . buffer ) . length > 0 ) { const reset = ( ) => { this . buffer . remove ( ) ; this . buffer = { } as Canvas ; if ( typeof callback === 'function' ) callback . call ( this ) ; } ; if ( Tools . throttle ) { setTimeout ( ( ) => { reset ( ) ; } , Tools . throttle ) ; } else reset ( ) ; } else if ( typeof callback === 'function' ) callback . call ( this ) ; } param x param y protected createPoint ( x , y ) { const rect = this . canvas . getCanvas ( ) . getBoundingClientRect ( ) ; return new Point ( Math . floor ( x - rect . left ) , Math . floor ( y - rect . top ) ) ; } return { MiPointGroup } protected createPointGroup ( ) { return { color : Tools . color , thickness : Tools . thickness , opacity : Tools . opacity , points : [ ] } ; } param element { HTMLDivElement } register ( element ) { for ( const i in this . events ) { if ( Object . prototype . hasOwnProperty . call ( this . events , i ) ) { this . events [ i ] . element = element ; } } } return protected toggleToolbar ( event ) { const point = this . createPoint ( event . clientX , event . clientY ) , container = Tools . getStage ( ) . container , offsetWidth = container . offsetWidth , offsetHeight = container . offsetHeight ; const hide = ( ) => { this . toolbar = false ; const tools = document . getElementById ( 'mi-canvas-tools' ) ; if ( tools ) { if ( this . G . mobile ) { tools . style . left = '-70px' ; tools . style . opacity = '0' ; } else { tools . style . bottom = '-70px' ; tools . style . opacity = '0' ; } } const footer = document . getElementById ( 'mi-canvas-footer' ) ; if ( footer ) { footer . style . bottom = '-70px' ; footer . style . opacity = '0' ; } } ; const show = ( ) => { this . toolbar = true ; const tools = document . getElementById ( 'mi-canvas-tools' ) ; if ( tools ) { if ( this . G . mobile ) { tools . style . left = '20px' ; tools . style . opacity = '1' ; } else { tools . style . bottom = '0' ; tools . style . opacity = '1' ; } } const footer = document . getElementById ( 'mi-canvas-footer' ) ; if ( footer ) { footer . style . bottom = '0' ; footer . style . opacity = '1' ; } } ; if ( this . G . mobile ) { if ( offsetWidth - point . x <= 300 ) { if ( this . drawing ) hide ( ) ; else if ( ! this . toolbar ) show ( ) ; } else { if ( ! this . toolbar || ! this . drawing ) show ( ) ; } } else { if ( offsetHeight - point . y <= 300 ) { if ( this . drawing ) hide ( ) ; else if ( ! this . toolbar ) show ( ) ; } else { if ( ! this . toolbar || ! this . drawing ) show ( ) ; } } } param event protected pointerdown ( event ) { if ( ! this . repeat ) { this . repeat = true ; this . drawing = true ; this . enter = false ; this . drawBegin ( event ) ; } } param event protected pointerenter ( event ) { if ( event . buttons === 1 && this . enter ) { this . drawing = false ; this . pointerdown ( event ) ; } } param event protected pointerleave ( event ) { if ( this . repeat ) this . pointerup ( event ) ; } param event protected pointermove ( event ) { if ( this . drawing ) { if ( this . toolbar ) this . toggleToolbar ( event ) ; this . drawProcess ( event ) ; } } param event protected pointerup ( event ) { if ( this . drawing ) { this . repeat = false ; this . drawing = false ; this . enter = true ; this . toggleToolbar ( event ) ; this . drawEnd ( event ) ; } } param event protected touchstart ( event ) { event . preventDefault ( ) ; if ( event . targetTouches . length === 1 ) { const touch = event . changedTouches [ 0 ] ; this . drawBegin ( touch ) ; } } param event protected touchmove ( event ) { event . preventDefault ( ) ; const touch = event . targetTouches [ 0 ] ; this . drawProcess ( touch ) ; } param event protected touchend ( event ) { const wasContainerTouched = event . target === Tools . stage . container ; if ( wasContainerTouched ) { event . preventDefault ( ) ; const touch = event . changedTouches [ 0 ] ; this . drawEnd ( touch ) ; } } param event protected abstract drawBegin ( event ) ; param event protected abstract drawUpdate ( event ) ; param event protected abstract drawEnd ( event ) ; }