l-painter.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. <template>
  2. <view class="lime-painter" ref="limepainter">
  3. <view v-if="canvasId && size" :style="styles">
  4. <!-- #ifndef APP-NVUE -->
  5. <canvas class="lime-painter__canvas" v-if="use2dCanvas" :id="canvasId" type="2d" :style="size"></canvas>
  6. <canvas class="lime-painter__canvas" v-else :id="canvasId" :canvas-id="canvasId" :style="size"
  7. :width="boardWidth * dpr" :height="boardHeight * dpr" :hidpi="hidpi"></canvas>
  8. <!-- #endif -->
  9. <!-- #ifdef APP-NVUE -->
  10. <web-view :style="size" ref="webview" src="/uni_modules/lime-painter/hybrid/html/index.html"
  11. class="lime-painter__canvas" @pagefinish="onPageFinish" @error="onError" @onPostMessage="onMessage">
  12. </web-view>
  13. <!-- #endif -->
  14. </view>
  15. <slot />
  16. </view>
  17. </template>
  18. <script>
  19. import { parent } from '../common/relation'
  20. import props from './props'
  21. import { toPx, base64ToPath, pathToBase64, isBase64, sleep, getImageInfo } from './utils';
  22. // #ifndef APP-NVUE
  23. import { canIUseCanvas2d, isPC } from './utils';
  24. import Painter from './painter';
  25. // import Painter from '@painter'
  26. const nvue = {}
  27. // #endif
  28. // #ifdef APP-NVUE
  29. import nvue from './nvue'
  30. // #endif
  31. export default {
  32. name: 'lime-painter',
  33. mixins: [props, parent('painter'), nvue],
  34. data() {
  35. return {
  36. use2dCanvas: false,
  37. canvasHeight: 150,
  38. canvasWidth: null,
  39. parentWidth: 0,
  40. inited: false,
  41. progress: 0,
  42. firstRender: 0,
  43. done: false,
  44. tasks: []
  45. };
  46. },
  47. computed: {
  48. styles() {
  49. return `${this.size}${this.customStyle || ''};` + (this.hidden && 'position: fixed; left: 1500rpx;')
  50. },
  51. canvasId() {
  52. return `l-painter${this._ && this._.uid || this._uid}`
  53. },
  54. size() {
  55. if (this.boardWidth && this.boardHeight) {
  56. return `width:${this.boardWidth}px; height: ${this.boardHeight}px;`;
  57. }
  58. },
  59. dpr() {
  60. return this.pixelRatio || uni.getSystemInfoSync().pixelRatio;
  61. },
  62. boardWidth() {
  63. const { width = 0 } = (this.elements && this.elements.css) || this.elements || this
  64. const w = toPx(width || this.width)
  65. return w || Math.max(w, toPx(this.canvasWidth));
  66. },
  67. boardHeight() {
  68. const { height = 0 } = (this.elements && this.elements.css) || this.elements || this
  69. const h = toPx(height || this.height)
  70. return h || Math.max(h, toPx(this.canvasHeight));
  71. },
  72. hasBoard() {
  73. return this.board && Object.keys(this.board).length
  74. },
  75. elements() {
  76. return this.hasBoard ? this.board : JSON.parse(JSON.stringify(this.el))
  77. }
  78. },
  79. created() {
  80. this.use2dCanvas = this.type === '2d' && canIUseCanvas2d() && !isPC
  81. },
  82. async mounted() {
  83. await sleep(30)
  84. await this.getParentWeith()
  85. this.$nextTick(() => {
  86. setTimeout(() => {
  87. this.$watch('elements', this.watchRender, {
  88. deep: true,
  89. immediate: true
  90. });
  91. }, 30)
  92. })
  93. },
  94. // #ifdef VUE3
  95. unmounted() {
  96. this.done = false
  97. this.inited = false
  98. this.firstRender = 0
  99. this.progress = 0
  100. this.painter = null
  101. clearTimeout(this.rendertimer)
  102. },
  103. // #endif
  104. // #ifdef VUE2
  105. destroyed() {
  106. this.done = false
  107. this.inited = false
  108. this.firstRender = 0
  109. this.progress = 0
  110. this.painter = null
  111. clearTimeout(this.rendertimer)
  112. },
  113. // #endif
  114. methods: {
  115. async watchRender(val, old) {
  116. if (!val || !val.views || (!this.firstRender ? !val.views.length : !this.firstRender) || !Object.keys(val).length || JSON.stringify(val) == JSON.stringify(old)) return;
  117. this.firstRender = 1
  118. this.progress = 0
  119. this.done = false
  120. clearTimeout(this.rendertimer)
  121. this.rendertimer = setTimeout(() => {
  122. this.render(val);
  123. }, this.beforeDelay)
  124. },
  125. async setFilePath(path, param) {
  126. let filePath = path
  127. const { pathType = this.pathType } = param || this
  128. if (pathType == 'base64' && !isBase64(path)) {
  129. filePath = await pathToBase64(path)
  130. } else if (pathType == 'url' && isBase64(path)) {
  131. filePath = await base64ToPath(path)
  132. }
  133. if (param && param.isEmit) {
  134. this.$emit('success', filePath);
  135. }
  136. return filePath
  137. },
  138. async getSize(args) {
  139. const { width } = args.css || args
  140. const { height } = args.css || args
  141. if (!this.size) {
  142. if (width || height) {
  143. this.canvasWidth = width || this.canvasWidth
  144. this.canvasHeight = height || this.canvasHeight
  145. await sleep(30);
  146. } else {
  147. await this.getParentWeith()
  148. }
  149. }
  150. },
  151. canvasToTempFilePathSync(args) {
  152. // this.stopWatch && this.stopWatch()
  153. // this.stopWatch = this.$watch('done', (v) => {
  154. // if (v) {
  155. // this.canvasToTempFilePath(args)
  156. // this.stopWatch && this.stopWatch()
  157. // }
  158. // }, {
  159. // immediate: true
  160. // })
  161. this.tasks.push(args)
  162. if (this.done) {
  163. this.runTask()
  164. }
  165. },
  166. runTask() {
  167. while (this.tasks.length) {
  168. const task = this.tasks.shift()
  169. this.canvasToTempFilePath(task)
  170. }
  171. },
  172. // #ifndef APP-NVUE
  173. getParentWeith() {
  174. return new Promise(resolve => {
  175. uni.createSelectorQuery()
  176. .in(this)
  177. .select(`.lime-painter`)
  178. .boundingClientRect()
  179. .exec(res => {
  180. const { width, height } = res[0] || {}
  181. this.parentWidth = Math.ceil(width || 0)
  182. this.canvasWidth = this.parentWidth || 300
  183. this.canvasHeight = height || this.canvasHeight || 150
  184. resolve(res[0])
  185. })
  186. })
  187. },
  188. async render(args = {}) {
  189. if (!Object.keys(args).length) {
  190. return console.error('空对象')
  191. }
  192. this.progress = 0
  193. this.done = false
  194. // #ifdef APP-NVUE
  195. this.tempFilePath.length = 0
  196. // #endif
  197. await this.getSize(args)
  198. const ctx = await this.getContext();
  199. let {
  200. use2dCanvas,
  201. boardWidth,
  202. boardHeight,
  203. canvas,
  204. afterDelay
  205. } = this;
  206. if (use2dCanvas && !canvas) {
  207. return Promise.reject(new Error('canvas 没创建'));
  208. }
  209. this.boundary = {
  210. top: 0,
  211. left: 0,
  212. width: boardWidth,
  213. height: boardHeight
  214. };
  215. this.painter = null
  216. if (!this.painter) {
  217. const { width } = args.css || args
  218. const { height } = args.css || args
  219. if (!width && this.parentWidth) {
  220. Object.assign(args, { width: this.parentWidth })
  221. }
  222. const param = {
  223. context: ctx,
  224. canvas,
  225. width: boardWidth,
  226. height: boardHeight,
  227. pixelRatio: this.dpr,
  228. useCORS: this.useCORS,
  229. createImage: getImageInfo.bind(this),
  230. performance: this.performance,
  231. listen: {
  232. onProgress: (v) => {
  233. this.progress = v
  234. this.$emit('progress', v)
  235. },
  236. onEffectFail: (err) => {
  237. this.$emit('faill', err)
  238. }
  239. }
  240. }
  241. this.painter = new Painter(param)
  242. }
  243. try {
  244. // vue3 赋值给data会引起图片无法绘制
  245. const { width, height } = await this.painter.source(JSON.parse(JSON.stringify(args)))
  246. this.boundary.height = this.canvasHeight = height
  247. this.boundary.width = this.canvasWidth = width
  248. await sleep(this.sleep);
  249. await this.painter.render()
  250. await new Promise(resolve => this.$nextTick(resolve));
  251. if (!use2dCanvas) {
  252. await this.canvasDraw();
  253. }
  254. if (afterDelay && use2dCanvas) {
  255. await sleep(afterDelay);
  256. }
  257. this.$emit('done');
  258. this.done = true
  259. if (this.isCanvasToTempFilePath) {
  260. this.canvasToTempFilePath()
  261. .then(res => {
  262. this.$emit('success', res.tempFilePath)
  263. })
  264. .catch(err => {
  265. this.$emit('fail', new Error(JSON.stringify(err)));
  266. });
  267. }
  268. this.runTask()
  269. return Promise.resolve({
  270. ctx,
  271. draw: this.painter,
  272. node: this.node
  273. });
  274. } catch (e) {
  275. //TODO handle the exception
  276. }
  277. },
  278. canvasDraw(flag = false) {
  279. return new Promise((resolve, reject) => this.ctx.draw(flag, () => setTimeout(() => resolve(), this
  280. .afterDelay)));
  281. },
  282. async getContext() {
  283. if (!this.canvasWidth) {
  284. this.$emit('fail', 'painter no size')
  285. console.error('[lime-painter]: 给画板或父级设置尺寸')
  286. return Promise.reject();
  287. }
  288. if (this.ctx && this.inited) {
  289. return Promise.resolve(this.ctx);
  290. }
  291. const { type, use2dCanvas, dpr, boardWidth, boardHeight } = this;
  292. const _getContext = () => {
  293. return new Promise(resolve => {
  294. uni.createSelectorQuery()
  295. .in(this)
  296. .select(`#${this.canvasId}`)
  297. .boundingClientRect()
  298. .exec(res => {
  299. if (res) {
  300. const ctx = uni.createCanvasContext(this.canvasId, this);
  301. if (!this.inited) {
  302. this.inited = true;
  303. this.use2dCanvas = false;
  304. this.canvas = res;
  305. }
  306. // 钉钉小程序框架不支持 measureText 方法,用此方法 mock
  307. if (!ctx.measureText) {
  308. function strLen(str) {
  309. let len = 0;
  310. for (let i = 0; i < str.length; i++) {
  311. if (str.charCodeAt(i) > 0 && str.charCodeAt(i) < 128) {
  312. len++;
  313. } else {
  314. len += 2;
  315. }
  316. }
  317. return len;
  318. }
  319. ctx.measureText = text => {
  320. let fontSize = ctx.state && ctx.state.fontSize || 12;
  321. const font = ctx.__font
  322. if (font && fontSize == 12) {
  323. fontSize = parseInt(font.split(' ')[3], 10);
  324. }
  325. fontSize /= 2;
  326. return {
  327. width: strLen(text) * fontSize
  328. };
  329. }
  330. }
  331. // #ifdef MP-ALIPAY
  332. ctx.scale(dpr, dpr);
  333. // #endif
  334. this.ctx = ctx
  335. resolve(this.ctx);
  336. } else {
  337. console.error('[lime-painter] no node')
  338. }
  339. });
  340. });
  341. };
  342. if (!use2dCanvas) {
  343. return _getContext();
  344. }
  345. return new Promise(resolve => {
  346. uni.createSelectorQuery()
  347. .in(this)
  348. .select(`#${this.canvasId}`)
  349. .node()
  350. .exec(res => {
  351. let { node: canvas } = res && res[0] || {};
  352. if (canvas) {
  353. const ctx = canvas.getContext(type);
  354. if (!this.inited) {
  355. this.inited = true;
  356. this.use2dCanvas = true;
  357. this.canvas = canvas;
  358. }
  359. this.ctx = ctx
  360. resolve(this.ctx);
  361. } else {
  362. console.error('[lime-painter]: no size')
  363. }
  364. });
  365. });
  366. },
  367. canvasToTempFilePath(args = {}) {
  368. return new Promise(async (resolve, reject) => {
  369. const { use2dCanvas, canvasId, dpr, fileType, quality } = this;
  370. const success = async (res) => {
  371. try {
  372. const tempFilePath = await this.setFilePath(res.tempFilePath || res, args)
  373. const result = Object.assign(res, { tempFilePath })
  374. args.success && args.success(result)
  375. resolve(result)
  376. } catch (e) {
  377. this.$emit('fail', e)
  378. }
  379. }
  380. let { top: y = 0, left: x = 0, width, height } = this.boundary || this;
  381. // let destWidth = width * dpr;
  382. // let destHeight = height * dpr;
  383. // #ifdef MP-ALIPAY
  384. // width = destWidth;
  385. // height = destHeight;
  386. // #endif
  387. const copyArgs = Object.assign({
  388. // x,
  389. // y,
  390. // width,
  391. // height,
  392. // destWidth,
  393. // destHeight,
  394. canvasId,
  395. id: canvasId,
  396. fileType,
  397. quality,
  398. }, args, { success });
  399. // if(this.isPC || use2dCanvas) {
  400. // copyArgs.canvas = this.canvas
  401. // }
  402. if (use2dCanvas) {
  403. copyArgs.canvas = this.canvas
  404. try {
  405. // #ifndef MP-ALIPAY
  406. const oFilePath = this.canvas.toDataURL(`image/${args.fileType || fileType}`.replace(/pg/, 'peg'), args.quality || quality)
  407. if (/data:,/.test(oFilePath)) {
  408. uni.canvasToTempFilePath(copyArgs, this);
  409. } else {
  410. const tempFilePath = await this.setFilePath(oFilePath, args)
  411. args.success && args.success({ tempFilePath })
  412. resolve({ tempFilePath })
  413. }
  414. // #endif
  415. // #ifdef MP-ALIPAY
  416. this.canvas.toTempFilePath(copyArgs)
  417. // #endif
  418. } catch (e) {
  419. args.fail && args.fail(e)
  420. reject(e)
  421. }
  422. } else {
  423. // #ifdef MP-ALIPAY
  424. if (this.ctx.toTempFilePath) {
  425. // 钉钉
  426. const ctx = uni.createCanvasContext(canvasId);
  427. ctx.toTempFilePath(copyArgs);
  428. } else {
  429. my.canvasToTempFilePath(copyArgs);
  430. }
  431. // #endif
  432. // #ifndef MP-ALIPAY
  433. uni.canvasToTempFilePath(copyArgs, this);
  434. // #endif
  435. }
  436. })
  437. }
  438. // #endif
  439. }
  440. };
  441. </script>
  442. <style lang="scss">
  443. .lime-painter,
  444. .lime-painter__canvas {
  445. // #ifndef APP-NVUE
  446. width: 100%;
  447. // #endif
  448. // #ifdef APP-NVUE
  449. flex: 1;
  450. // #endif
  451. }
  452. </style>