l-painter.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  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 :canvas-id="canvasId" :style="size" :id="canvasId"
  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. };
  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. },
  162. // #ifndef APP-NVUE
  163. getParentWeith() {
  164. return new Promise(resolve => {
  165. uni.createSelectorQuery()
  166. .in(this)
  167. .select(`.lime-painter`)
  168. .boundingClientRect()
  169. .exec(res => {
  170. const {width, height} = res[0]||{}
  171. this.parentWidth = Math.ceil(width||0)
  172. this.canvasWidth = this.parentWidth || 300
  173. this.canvasHeight = height || this.canvasHeight||150
  174. resolve(res[0])
  175. })
  176. })
  177. },
  178. async render(args = {}) {
  179. if(!Object.keys(args).length) {
  180. return console.error('空对象')
  181. }
  182. this.progress = 0
  183. this.done = false
  184. // #ifdef APP-NVUE
  185. this.tempFilePath.length = 0
  186. // #endif
  187. await this.getSize(args)
  188. const ctx = await this.getContext();
  189. let {
  190. use2dCanvas,
  191. boardWidth,
  192. boardHeight,
  193. canvas,
  194. afterDelay
  195. } = this;
  196. if (use2dCanvas && !canvas) {
  197. return Promise.reject(new Error('canvas 没创建'));
  198. }
  199. this.boundary = {
  200. top: 0,
  201. left: 0,
  202. width: boardWidth,
  203. height: boardHeight
  204. };
  205. this.painter = null
  206. if (!this.painter) {
  207. const {width} = args.css || args
  208. const {height} = args.css || args
  209. if(!width && this.parentWidth) {
  210. Object.assign(args, {width: this.parentWidth})
  211. }
  212. const param = {
  213. context: ctx,
  214. canvas,
  215. width: boardWidth,
  216. height: boardHeight,
  217. pixelRatio: this.dpr,
  218. useCORS: this.useCORS,
  219. createImage: getImageInfo.bind(this),
  220. performance: this.performance,
  221. listen: {
  222. onProgress: (v) => {
  223. this.progress = v
  224. this.$emit('progress', v)
  225. },
  226. onEffectFail: (err) => {
  227. this.$emit('faill', err)
  228. }
  229. }
  230. }
  231. this.painter = new Painter(param)
  232. }
  233. // vue3 赋值给data会引起图片无法绘制
  234. const { width, height } = await this.painter.source(JSON.parse(JSON.stringify(args)))
  235. this.boundary.height = this.canvasHeight = height
  236. this.boundary.width = this.canvasWidth = width
  237. await sleep(this.sleep);
  238. await this.painter.render()
  239. await new Promise(resolve => this.$nextTick(resolve));
  240. if (!use2dCanvas) {
  241. await this.canvasDraw();
  242. }
  243. if (afterDelay && use2dCanvas) {
  244. await sleep(afterDelay);
  245. }
  246. this.$emit('done');
  247. this.done = true
  248. if (this.isCanvasToTempFilePath) {
  249. this.canvasToTempFilePath()
  250. .then(res => {
  251. this.$emit('success', res.tempFilePath)
  252. })
  253. .catch(err => {
  254. this.$emit('fail', new Error(JSON.stringify(err)));
  255. });
  256. }
  257. return Promise.resolve({
  258. ctx,
  259. draw: this.painter,
  260. node: this.node
  261. });
  262. },
  263. canvasDraw(flag = false) {
  264. return new Promise((resolve, reject) => this.ctx.draw(flag, () => setTimeout(() => resolve(), this
  265. .afterDelay)));
  266. },
  267. async getContext() {
  268. if (!this.canvasWidth) {
  269. this.$emit('fail', 'painter no size')
  270. console.error('[lime-painter]: 给画板或父级设置尺寸')
  271. return Promise.reject();
  272. }
  273. if (this.ctx && this.inited) {
  274. return Promise.resolve(this.ctx);
  275. }
  276. const { type, use2dCanvas, dpr, boardWidth, boardHeight } = this;
  277. const _getContext = () => {
  278. return new Promise(resolve => {
  279. uni.createSelectorQuery()
  280. .in(this)
  281. .select(`#${this.canvasId}`)
  282. .boundingClientRect()
  283. .exec(res => {
  284. if (res) {
  285. const ctx = uni.createCanvasContext(this.canvasId, this);
  286. if (!this.inited) {
  287. this.inited = true;
  288. this.use2dCanvas = false;
  289. this.canvas = res;
  290. }
  291. // 钉钉小程序框架不支持 measureText 方法,用此方法 mock
  292. if (!ctx.measureText) {
  293. function strLen(str) {
  294. let len = 0;
  295. for (let i = 0; i < str.length; i++) {
  296. if (str.charCodeAt(i) > 0 && str.charCodeAt(i) < 128) {
  297. len++;
  298. } else {
  299. len += 2;
  300. }
  301. }
  302. return len;
  303. }
  304. ctx.measureText = text => {
  305. let fontSize = ctx.state && ctx.state.fontSize || 12;
  306. const font = ctx.__font
  307. if (font && fontSize == 12) {
  308. fontSize = parseInt(font.split(' ')[3], 10);
  309. }
  310. fontSize /= 2;
  311. return {
  312. width: strLen(text) * fontSize
  313. };
  314. }
  315. }
  316. // #ifdef MP-ALIPAY
  317. ctx.scale(dpr, dpr);
  318. // #endif
  319. this.ctx = ctx
  320. resolve(this.ctx);
  321. } else {
  322. console.error('[lime-painter] no node')
  323. }
  324. });
  325. });
  326. };
  327. if (!use2dCanvas) {
  328. return _getContext();
  329. }
  330. return new Promise(resolve => {
  331. uni.createSelectorQuery()
  332. .in(this)
  333. .select(`#${this.canvasId}`)
  334. .node()
  335. .exec(res => {
  336. let {node: canvas} = res && res[0]||{};
  337. if(canvas) {
  338. const ctx = canvas.getContext(type);
  339. if (!this.inited) {
  340. this.inited = true;
  341. this.use2dCanvas = true;
  342. this.canvas = canvas;
  343. }
  344. this.ctx = ctx
  345. resolve(this.ctx);
  346. } else {
  347. console.error('[lime-painter]: no size')
  348. }
  349. });
  350. });
  351. },
  352. canvasToTempFilePath(args = {}) {
  353. return new Promise(async (resolve, reject) => {
  354. const { use2dCanvas, canvasId, dpr, fileType, quality } = this;
  355. const success = async (res) => {
  356. try {
  357. const tempFilePath = await this.setFilePath(res.tempFilePath || res, args)
  358. const result = Object.assign(res, {tempFilePath})
  359. args.success && args.success(result)
  360. resolve(result)
  361. } catch (e) {
  362. this.$emit('fail', e)
  363. }
  364. }
  365. let { top: y = 0, left: x = 0, width, height } = this.boundary || this;
  366. // let destWidth = width * dpr;
  367. // let destHeight = height * dpr;
  368. // #ifdef MP-ALIPAY
  369. // width = destWidth;
  370. // height = destHeight;
  371. // #endif
  372. const copyArgs = Object.assign({
  373. // x,
  374. // y,
  375. // width,
  376. // height,
  377. // destWidth,
  378. // destHeight,
  379. canvasId,
  380. id: canvasId,
  381. fileType,
  382. quality,
  383. }, args, {success});
  384. // if(this.isPC || use2dCanvas) {
  385. // copyArgs.canvas = this.canvas
  386. // }
  387. if (use2dCanvas) {
  388. copyArgs.canvas = this.canvas
  389. try{
  390. // #ifndef MP-ALIPAY
  391. const oFilePath = this.canvas.toDataURL(`image/${args.fileType||fileType}`.replace(/pg/, 'peg'), args.quality||quality)
  392. if(/data:,/.test(oFilePath)) {
  393. uni.canvasToTempFilePath(copyArgs, this);
  394. } else {
  395. const tempFilePath = await this.setFilePath(oFilePath, args)
  396. args.success && args.success({tempFilePath})
  397. resolve({tempFilePath})
  398. }
  399. // #endif
  400. // #ifdef MP-ALIPAY
  401. this.canvas.toTempFilePath(copyArgs)
  402. // #endif
  403. }catch(e){
  404. args.fail && args.fail(e)
  405. reject(e)
  406. }
  407. } else {
  408. // #ifdef MP-ALIPAY
  409. if(this.ctx.toTempFilePath) {
  410. // 钉钉
  411. const ctx = uni.createCanvasContext(canvasId);
  412. ctx.toTempFilePath(copyArgs);
  413. } else {
  414. my.canvasToTempFilePath(copyArgs);
  415. }
  416. // #endif
  417. // #ifndef MP-ALIPAY
  418. uni.canvasToTempFilePath(copyArgs, this);
  419. // #endif
  420. }
  421. })
  422. }
  423. // #endif
  424. }
  425. };
  426. </script>
  427. <style>
  428. .lime-painter,
  429. .lime-painter__canvas {
  430. // #ifndef APP-NVUE
  431. width: 100%;
  432. // #endif
  433. // #ifdef APP-NVUE
  434. flex: 1;
  435. // #endif
  436. }
  437. </style>