l-painter.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  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"></canvas>
  8. <!-- #endif -->
  9. <!-- #ifdef APP-NVUE -->
  10. <web-view :style="size" ref="webview"
  11. src="/uni_modules/lime-painter/static/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 { compareVersion } from './utils';
  25. import Painter from './painter'
  26. // import Painter from '@lime/'
  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. // #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
  38. use2dCanvas: true,
  39. // #endif
  40. // #ifndef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
  41. use2dCanvas: false,
  42. // #endif
  43. canvasHeight: 150,
  44. canvasWidth: null,
  45. parentWidth: 0,
  46. inited: false,
  47. progress: 0,
  48. firstRender: 0,
  49. done: false
  50. };
  51. },
  52. computed: {
  53. styles() {
  54. return `${this.size}${this.customStyle||''};`
  55. },
  56. canvasId() {
  57. return `l-painter${this._uid || this._.uid}`
  58. },
  59. size() {
  60. if (this.boardWidth && this.boardHeight) {
  61. return `width:${this.boardWidth}px; height: ${this.boardHeight}px;`;
  62. }
  63. },
  64. dpr() {
  65. return this.pixelRatio || uni.getSystemInfoSync().pixelRatio;
  66. },
  67. boardWidth() {
  68. const {width = 0} = (this.elements && this.elements.css) || this.elements || this
  69. const w = toPx(width||this.width)
  70. return w || Math.max(w, toPx(this.canvasWidth));
  71. },
  72. boardHeight() {
  73. const {height = 0} = (this.elements && this.elements.css) || this.elements || this
  74. const h = toPx(height||this.height)
  75. return h || Math.max(h, toPx(this.canvasHeight));
  76. },
  77. hasBoard() {
  78. return this.board && Object.keys(this.board).length
  79. },
  80. elements() {
  81. return this.hasBoard ? this.board : JSON.parse(JSON.stringify(this.el))
  82. }
  83. },
  84. watch: {
  85. // #ifdef MP-WEIXIN || MP-ALIPAY
  86. size(v) {
  87. // #ifdef MP-WEIXIN
  88. if (this.use2dCanvas) {
  89. this.inited = false;
  90. }
  91. // #endif
  92. // #ifdef MP-ALIPAY
  93. this.inited = false;
  94. // #endif
  95. },
  96. // #endif
  97. },
  98. created() {
  99. const { SDKVersion, version, platform } = uni.getSystemInfoSync();
  100. // #ifdef MP-WEIXIN
  101. this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '2.9.2') >= 0 && !this.isPC;
  102. // #endif
  103. // #ifdef MP-TOUTIAO
  104. this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '1.78.0') >= 0;
  105. // #endif
  106. // #ifdef MP-ALIPAY
  107. this.use2dCanvas = this.type === '2d' && compareVersion(my.SDKVersion, '2.7.15') >= 0;
  108. // #endif
  109. },
  110. async mounted() {
  111. await sleep(30)
  112. await this.getParentWeith()
  113. this.$nextTick(() => {
  114. setTimeout(() => {
  115. this.$watch('elements', this.watchRender, {
  116. deep: true,
  117. immediate: true
  118. });
  119. }, 30)
  120. })
  121. },
  122. methods: {
  123. async watchRender(val, old) {
  124. if (!val || !val.views || (!this.firstRender ? !val.views.length : !this.firstRender) || !Object.keys(val).length || JSON.stringify(val) == JSON.stringify(old)) return;
  125. this.firstRender = 1
  126. clearTimeout(this.rendertimer)
  127. this.rendertimer = setTimeout(() => {
  128. this.render(val);
  129. }, this.beforeDelay)
  130. },
  131. async setFilePath(path, param) {
  132. let filePath = path
  133. const {pathType = this.pathType} = param || this
  134. if (pathType == 'base64' && !isBase64(path)) {
  135. filePath = await pathToBase64(path)
  136. } else if (pathType == 'url' && isBase64(path)) {
  137. filePath = await base64ToPath(path)
  138. }
  139. if (param && param.isEmit) {
  140. this.$emit('success', filePath);
  141. }
  142. return filePath
  143. },
  144. async getSize(args) {
  145. const {width} = args.css || args
  146. const {height} = args.css || args
  147. if (!this.size) {
  148. if (width || height) {
  149. this.canvasWidth = width || this.canvasWidth
  150. this.canvasHeight = height || this.canvasHeight
  151. await sleep(30);
  152. } else {
  153. await this.getParentWeith()
  154. }
  155. }
  156. },
  157. canvasToTempFilePathSync(args) {
  158. this.stopWatch = this.$watch('done', (v) => {
  159. if (v) {
  160. this.canvasToTempFilePath(args)
  161. this.stopWatch && this.stopWatch()
  162. }
  163. }, {
  164. immediate: true
  165. })
  166. },
  167. // #ifndef APP-NVUE
  168. getParentWeith() {
  169. return new Promise(resolve => {
  170. uni.createSelectorQuery()
  171. .in(this)
  172. .select(`.lime-painter`)
  173. .boundingClientRect()
  174. .exec(res => {
  175. const {width, height} = res[0]||{}
  176. this.parentWidth = Math.ceil(width||0)
  177. this.canvasWidth = this.parentWidth || 300
  178. this.canvasHeight = height || this.canvasHeight||150
  179. resolve(res[0])
  180. })
  181. })
  182. },
  183. async render(args = {}) {
  184. if(!Object.keys(args).length) {
  185. return console.error('空对象')
  186. }
  187. this.progress = 0
  188. this.done = false
  189. await this.getSize(args)
  190. const ctx = await this.getContext();
  191. let {
  192. use2dCanvas,
  193. boardWidth,
  194. boardHeight,
  195. canvas,
  196. afterDelay
  197. } = this;
  198. if (use2dCanvas && !canvas) {
  199. return Promise.reject(new Error('render: fail canvas has not been created'));
  200. }
  201. this.boundary = {
  202. top: 0,
  203. left: 0,
  204. width: boardWidth,
  205. height: boardHeight
  206. };
  207. this.painter = null
  208. if (!this.painter) {
  209. const {width} = args.css || args
  210. const {height} = args.css || args
  211. if(!width && this.parentWidth) {
  212. Object.assign(args, {width: this.parentWidth})
  213. }
  214. const param = {
  215. context: ctx,
  216. canvas,
  217. width: boardWidth,
  218. height: boardHeight,
  219. pixelRatio: this.dpr,
  220. useCORS: this.useCORS,
  221. createImage: getImageInfo.bind(this),
  222. listen: {
  223. onProgress: (v) => {
  224. this.progress = v
  225. this.$emit('progress', v)
  226. },
  227. onEffectFail: (err) => {
  228. this.$emit('faill', err)
  229. }
  230. }
  231. }
  232. this.painter = new Painter(param)
  233. }
  234. // vue3 赋值给data会引起图片无法绘制
  235. const { width, height } = await this.painter.source(JSON.parse(JSON.stringify(args)))
  236. this.boundary.height = this.canvasHeight = height
  237. this.boundary.width = this.canvasWidth = width
  238. await sleep(this.sleep);
  239. // 可能会因为尺寸改变影响绘制上下文
  240. this.painter.setContext(this.ctx)
  241. await this.painter.render()
  242. await new Promise(resolve => this.$nextTick(resolve));
  243. if (!use2dCanvas) {
  244. await this.canvasDraw();
  245. }
  246. if (afterDelay && use2dCanvas) {
  247. await sleep(afterDelay);
  248. }
  249. this.$emit('done');
  250. this.done = true
  251. if (this.isCanvasToTempFilePath) {
  252. this.canvasToTempFilePath()
  253. .then(res => {
  254. this.$emit('success', res.tempFilePath)
  255. })
  256. .catch(err => {
  257. this.$emit('fail', new Error(JSON.stringify(err)));
  258. });
  259. }
  260. return Promise.resolve({
  261. ctx,
  262. draw: this.painter,
  263. node: this.node
  264. });
  265. },
  266. canvasDraw(flag = false) {
  267. return new Promise((resolve, reject) => this.ctx.draw(flag, () => setTimeout(() => resolve(), this
  268. .afterDelay)));
  269. },
  270. async getContext() {
  271. if (!this.canvasWidth) {
  272. this.$emit('fail', 'painter no size')
  273. console.error('painter no size: 请给画板或父级设置尺寸')
  274. return Promise.reject();
  275. }
  276. if (this.ctx && this.inited) {
  277. return Promise.resolve(this.ctx);
  278. }
  279. const { type, use2dCanvas, dpr, boardWidth, boardHeight } = this;
  280. const _getContext = () => {
  281. return new Promise(resolve => {
  282. uni.createSelectorQuery()
  283. .in(this)
  284. .select(`#${this.canvasId}`)
  285. .boundingClientRect()
  286. .exec(res => {
  287. if (res) {
  288. const ctx = uni.createCanvasContext(this.canvasId, this);
  289. if (!this.inited) {
  290. this.inited = true;
  291. this.use2dCanvas = false;
  292. this.canvas = res;
  293. }
  294. // #ifdef MP-ALIPAY
  295. ctx.scale(dpr, dpr);
  296. // #endif
  297. this.ctx = ctx
  298. resolve(this.ctx);
  299. }
  300. });
  301. });
  302. };
  303. if (!use2dCanvas) {
  304. return _getContext();
  305. }
  306. return new Promise(resolve => {
  307. uni.createSelectorQuery()
  308. .in(this)
  309. .select(`#${this.canvasId}`)
  310. .node()
  311. .exec(res => {
  312. let {node: canvas} = res[0];
  313. if (!canvas) {
  314. this.use2dCanvas = false;
  315. resolve(this.getContext());
  316. }
  317. const ctx = canvas.getContext(type);
  318. if (!this.inited) {
  319. this.inited = true;
  320. this.use2dCanvas = true;
  321. this.canvas = canvas;
  322. }
  323. this.ctx = ctx
  324. resolve(this.ctx);
  325. });
  326. });
  327. },
  328. canvasToTempFilePath(args = {}) {
  329. return new Promise(async (resolve, reject) => {
  330. const { use2dCanvas, canvasId, dpr, fileType, quality } = this;
  331. const success = async (res) => {
  332. try {
  333. const tempFilePath = await this.setFilePath(res.tempFilePath || res)
  334. resolve(Object.assign(res, {tempFilePath}))
  335. } catch (e) {
  336. this.$emit('fail', e)
  337. }
  338. }
  339. let { top: y = 0, left: x = 0, width, height } = this.boundary || this;
  340. let destWidth = width * dpr;
  341. let destHeight = height * dpr;
  342. // #ifdef MP-ALIPAY
  343. width = destWidth;
  344. height = destHeight;
  345. // #endif
  346. const copyArgs = Object.assign({
  347. x,
  348. y,
  349. width,
  350. height,
  351. destWidth,
  352. destHeight,
  353. canvasId,
  354. fileType,
  355. quality,
  356. success,
  357. fail: reject
  358. }, args);
  359. if (use2dCanvas) {
  360. try{
  361. // #ifndef MP-ALIPAY
  362. if(!args.pathType && !this.pathType) {args.pathType = 'url'}
  363. const tempFilePath = await this.setFilePath(this.canvas.toDataURL(`image/${args.fileType||fileType}`.replace(/pg/, 'peg'), args.quality||quality), args)
  364. args.success && args.success({tempFilePath})
  365. resolve({tempFilePath})
  366. // #endif
  367. // #ifdef MP-ALIPAY
  368. this.canvas.toTempFilePath(copyArgs)
  369. // #endif
  370. }catch(e){
  371. args.fail && args.fail(e)
  372. reject(e)
  373. }
  374. } else {
  375. // #ifdef MP-ALIPAY
  376. uni.canvasToTempFilePath(copyArgs);
  377. // #endif
  378. // #ifndef MP-ALIPAY
  379. uni.canvasToTempFilePath(copyArgs, this);
  380. // #endif
  381. }
  382. })
  383. }
  384. // #endif
  385. }
  386. };
  387. </script>
  388. <style>
  389. .lime-painter,
  390. .lime-painter__canvas {
  391. // #ifndef APP-NVUE
  392. width: 100%;
  393. // #endif
  394. // #ifdef APP-NVUE
  395. flex: 1;
  396. // #endif
  397. }
  398. </style>