wike-painter.vue 11 KB

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