l-painter.vue 12 KB

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