draw-poster.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import uni from "./utils/global"
  2. import {
  3. Canvas, Execute,
  4. DrawPosterCanvasCtx,
  5. CreateImagePathOptions,
  6. DrawPosterBuildOpts,
  7. DrawPosterUseOpts,
  8. drawPosterExtends,
  9. DrawPosterUseCtxOpts
  10. } from "./utils/interface"
  11. import { handleBuildOpts, extendMount } from "./utils/utils"
  12. import { getCanvas2dContext } from "./utils/wx-utils"
  13. // 实例类型添加上扩展类型
  14. type DrawPosterInstanceType = InstanceType<typeof DrawPoster> & drawPosterExtends
  15. // 扩展挂载储存
  16. let drawPosterExtend: Record<any, any> = {}
  17. let drawCtxPosterExtend: Record<any, any> = {}
  18. class DrawPoster {
  19. [key: string]: any
  20. private executeOnions = [] as Execute
  21. private stopStatus = false
  22. private drawType: 'type2d' | 'context'
  23. /** 构建器, 构建返回当前实例, 并挂载多个方法 */
  24. constructor(
  25. public canvas: Canvas,
  26. public ctx: DrawPosterCanvasCtx,
  27. public canvasId: string,
  28. public loading: boolean,
  29. public debugging: boolean,
  30. public loadingText: string,
  31. public createText: string,
  32. tips: boolean,
  33. ) {
  34. if (!canvas || !ctx || !canvasId) {
  35. throw new Error("DrawPoster Error: Use DrawPoster.build(string | ops) to build drawPoster instance objects")
  36. }
  37. // 判断当前绘制类型
  38. ctx.drawType = this.drawType = (ctx.draw) ? 'context' : 'type2d'
  39. // 挂载全局实例, 绘画扩展
  40. extendMount(this.ctx, drawCtxPosterExtend, (extend, target) => {
  41. target?.init?.(this.canvas, this.ctx)
  42. return (...args: any[]) => extend(this.canvas, this.ctx, ...args)
  43. })
  44. extendMount(this, drawPosterExtend, (extend, target) => {
  45. target?.init?.(this)
  46. return (...args: any[]) => extend(this, ...args)
  47. })
  48. // 当离开页面时, 自动调用停止绘画
  49. const _this = this
  50. const pages = getCurrentPages()
  51. const page = pages[pages.length - 1] as Record<any, any>
  52. // 查询标识, 不存在, 在替换页面卸载回调, 避免产生死循环
  53. if (!page?.onUnload?.identification) {
  54. page.oldOnUnload = page.onUnload
  55. page.onUnload = function () {
  56. _this?.stop()
  57. page.oldOnUnload()
  58. }
  59. page.onUnload.identification = true
  60. }
  61. tips && this.debuggingLog('构建完成', {canvas, ctx, selector: canvasId}, '#19be6b')
  62. }
  63. /** 提示器, 传入消息与数据 */
  64. private debuggingLog = (message: string, data?: any, color = "#3489fd") => {
  65. if (this.debugging) {
  66. if (data) {
  67. console.log(`%c${this.canvasId} -> ${message}`, `color: ${color}`, data)
  68. } else {
  69. console.log(`%c${this.canvasId} -> ${message}`, `color: ${color}`)
  70. }
  71. }
  72. }
  73. /** 传入挂载配置对象, 添加扩展方法 */
  74. static use = (opts: DrawPosterUseOpts) => {
  75. if (opts.name) drawPosterExtend[opts.name] = opts
  76. }
  77. /** 传入挂载配置对象, 添加绘画扩展方法 */
  78. static useCtx = (opts: DrawPosterUseCtxOpts) => {
  79. if (opts.name) drawCtxPosterExtend[opts.name] = opts
  80. }
  81. /** 构建绘制海报矩形方法, 传入canvas选择器或配置对象, 返回绘制对象 */
  82. static build = async (options: string | DrawPosterBuildOpts, tips = true) => {
  83. const config = handleBuildOpts(options)
  84. // 初始化监测当前页面绘制对象
  85. const pages = getCurrentPages()
  86. const page = pages[pages.length - 1] as Record<string, DrawPosterInstanceType>
  87. const gcanvas: drawPosterExtends['gcanvas'] = DrawPoster.prototype['gcanvas']
  88. if (page[config.selector + '__dp']) {
  89. return page[config.selector + '__dp']
  90. }
  91. if (config.gcanvas) {
  92. if (!gcanvas)
  93. console.error('--- 当前未引入gcanvas扩展, 将自动切换为普通 canvas ---')
  94. else
  95. gcanvas.enable(config.componentThis?.$refs?.[config.selector], {
  96. bridge: gcanvas.WeexBridge
  97. })
  98. }
  99. // 获取canvas实例
  100. const canvas = config.gcanvas && gcanvas ?
  101. gcanvas.enable(config.componentThis?.$refs?.[config.selector], {
  102. bridge: gcanvas.WeexBridge
  103. }) :
  104. await getCanvas2dContext(config.selector, config.componentThis) as Canvas
  105. const ctx = (
  106. canvas.getContext?.("2d") || uni.createCanvasContext(config.selector, config.componentThis)
  107. ) as DrawPosterCanvasCtx
  108. const dp = new DrawPoster(
  109. canvas, ctx,
  110. config.selector,
  111. config.loading,
  112. config.debugging,
  113. config.loadingText,
  114. config.createText,
  115. tips
  116. )
  117. // 储存当前绘制对象
  118. page[config.selector + '__dp'] = dp as DrawPosterInstanceType;
  119. return page[config.selector + '__dp']
  120. }
  121. /** 构建多个绘制海报矩形方法, 传入选择器或配置对象的数组, 返回多个绘制对象 */
  122. static buildAll = async (optionsAll: (string | DrawPosterBuildOpts)[]) => {
  123. const dpsArr = await Promise.all(optionsAll.map(async options => {
  124. return await DrawPoster.build(options, false)
  125. }))
  126. const dpsObj = {} as { [key: string]: typeof dpsArr[0] }
  127. dpsArr.forEach(dp => dpsObj[dp.canvasId] = dp)
  128. console.log("%cdraw-poster 构建完成:", "#E3712A", dpsObj)
  129. return dpsObj
  130. }
  131. /** 绘制器, 接收执行器函数, 添加到绘制容器中 */
  132. public draw = (execute: (ctx: DrawPosterCanvasCtx) => Promise<any> | void) => {
  133. const length = this.executeOnions.length
  134. this.executeOnions.push(async () => {
  135. try {
  136. this.ctx.save()
  137. await execute(this.ctx)
  138. this.ctx.restore()
  139. return true
  140. } catch (error) {
  141. const isOutError = error?.message?.search?.(`'nodeId' of undefined`) >= 0
  142. !isOutError && console.error(`${this.canvasId} -> 绘画栈(${length}),绘制错误:`, error)
  143. return false
  144. }
  145. })
  146. }
  147. /** 等待创建绘画, 成功后清空绘制器容器 */
  148. public awaitCreate = async (): Promise<boolean[]> => {
  149. this.debuggingLog('绘制海报中...')
  150. this.loading && uni.showLoading({ title: this.loadingText })
  151. const tips: Array<boolean> = []
  152. for (let i = 0; i < this.executeOnions.length; i++) {
  153. const execute = this.executeOnions[i]
  154. tips.push(await execute())
  155. }
  156. this.executeOnions = []
  157. this.debuggingLog('绘制状况', tips)
  158. // 当前绘制为 type2 绘制
  159. if (this.drawType === 'type2d') {
  160. this.loading && uni.hideLoading()
  161. return tips
  162. }
  163. // 当前绘制为 context 绘制
  164. return await new Promise((resolve) => {
  165. this.ctx.draw(true, () => {
  166. resolve(tips)
  167. this.loading && uni.hideLoading()
  168. })
  169. })
  170. }
  171. /** 创建canvas本地地址 @returns {string} 本地地址 */
  172. public createImagePath = async (baseOptions: CreateImagePathOptions = {}): Promise<string> => {
  173. const { canvas, canvasId, executeOnions, awaitCreate } = this
  174. executeOnions.length && await awaitCreate()
  175. // 如果当前为停止状态
  176. if (this.stopStatus) {
  177. this.stopStatus = false
  178. return '---stop createImagePath---'
  179. }
  180. this.loading && uni.showLoading({ title: this.createText })
  181. const options: WechatMiniprogram.CanvasToTempFilePathOption = {
  182. // x: 0, y: 0,
  183. // width: canvas.width,
  184. // height: canvas.height,
  185. // destWidth: canvas.width * 2,
  186. // destHeight: canvas.height * 2,
  187. ...baseOptions
  188. };
  189. if (this.drawType === 'context')
  190. options.canvasId = canvasId
  191. if (this.drawType === 'type2d')
  192. options.canvas = canvas
  193. return new Promise((resolve, reject) => {
  194. options.success = (res) => {
  195. resolve(res.tempFilePath)
  196. this.loading && uni.hideLoading();
  197. this.debuggingLog('绘制成功 🎉', res, '#19be6b')
  198. }
  199. options.fail = (err) => {
  200. reject(err)
  201. this.loading && uni.hideLoading();
  202. this.debuggingLog('绘制失败 🌟', err, '#fa3534')
  203. }
  204. uni.canvasToTempFilePath(options as any)
  205. })
  206. }
  207. /** 停止当前绘画, 调用则停止当前绘画堆栈的绘画 */
  208. public stop = () => {
  209. this.executeOnions = []
  210. this.stopStatus = true
  211. }
  212. }
  213. export default DrawPoster;