l-painter.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. <template>
  2. <view class="lime-painter" ref="limepainter">
  3. <view v-if="canvasId && size" :style="size + customStyle">
  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 v-if="hybrid || isInitFile" :style="size" ref="webview"
  11. :src="hybrid ? '/hybrid/html/lime-painter/index.html' :'_doc/uni_modules/lime-painter/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 {
  21. parent
  22. } from '../common/relation'
  23. // #ifndef APP-NVUE
  24. import {
  25. toPx,
  26. compareVersion,
  27. sleep,
  28. base64ToPath,
  29. pathToBase64,
  30. isBase64
  31. } from './utils';
  32. import Painter from './painter'
  33. // #endif
  34. // #ifdef APP-NVUE
  35. import {
  36. toPx,
  37. sleep,
  38. base64ToPath,
  39. pathToBase64,
  40. getImageInfo,
  41. isBase64,
  42. useNvue
  43. } from './utils';
  44. const dom = weex.requireModule('dom')
  45. import {
  46. version
  47. } from '../../package.json'
  48. // #endif
  49. export default {
  50. name: 'lime-painter',
  51. mixins: [parent('painter')],
  52. props: {
  53. board: Object,
  54. pathType: String, // 'base64'、'url'
  55. fileType: {
  56. type: String,
  57. default: 'png'
  58. },
  59. quality: {
  60. type: Number,
  61. default: 1
  62. },
  63. css: [String, Object],
  64. width: [Number, String],
  65. height: [Number, String],
  66. pixelRatio: Number,
  67. customStyle: String,
  68. isCanvasToTempFilePath: Boolean,
  69. sleep: {
  70. type: Number,
  71. default: 1000 / 30
  72. },
  73. beforeDelay: {
  74. type: Number,
  75. default: 100
  76. },
  77. afterDelay: {
  78. type: Number,
  79. default: 100
  80. },
  81. // #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
  82. type: {
  83. type: String,
  84. default: '2d'
  85. },
  86. // #endif
  87. // #ifdef APP-NVUE
  88. hybrid: Boolean,
  89. timeout: {
  90. type: Number,
  91. default: 2000
  92. }
  93. // #endif
  94. },
  95. data() {
  96. return {
  97. // #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
  98. use2dCanvas: true,
  99. // #endif
  100. // #ifndef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
  101. use2dCanvas: false,
  102. // #endif
  103. canvasHeight: 150,
  104. canvasWidth: null,
  105. isPC: false,
  106. inited: false,
  107. progress: 0,
  108. // #ifdef APP-NVUE
  109. tempFilePath: [],
  110. isInitFile: false
  111. // #endif
  112. };
  113. },
  114. computed: {
  115. canvasId() {
  116. // #ifdef VUE3
  117. return `l-painter${this._.uid}`
  118. // #endif
  119. // #ifdef VUE2
  120. return `l-painter${this._uid}`
  121. // #endif
  122. },
  123. size() {
  124. if (this.boardWidth && this.boardHeight) {
  125. return `width:${this.boardWidth}px; height: ${this.boardHeight}px;`;
  126. }
  127. },
  128. dpr() {
  129. return this.pixelRatio || uni.getSystemInfoSync().pixelRatio;
  130. },
  131. boardWidth() {
  132. const {
  133. width = 0
  134. } = (this.board && this.board.css) || this.board || this
  135. return toPx(width) || Math.max(toPx(width), toPx(this.canvasWidth));
  136. },
  137. boardHeight() {
  138. const {
  139. height = 0
  140. } = (this.board && this.board.css) || this.board || this
  141. return toPx(height) || Math.max(toPx(height), toPx(this.canvasHeight));
  142. },
  143. elements() {
  144. return JSON.parse(JSON.stringify(this.el))
  145. }
  146. },
  147. watch: {
  148. canvasWidth(v) {
  149. if (this.el.css && !this.el.css.width) {
  150. this.el.css.width = v
  151. }
  152. },
  153. // #ifdef MP-WEIXIN || MP-ALIPAY
  154. size(v) {
  155. // #ifdef MP-WEIXIN
  156. if (this.use2dCanvas) {
  157. this.inited = false;
  158. }
  159. // #endif
  160. // #ifdef MP-ALIPAY
  161. this.inited = false;
  162. // #endif
  163. },
  164. // #endif
  165. },
  166. // #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
  167. created() {
  168. const {
  169. SDKVersion,
  170. version,
  171. platform,
  172. environment
  173. } = uni.getSystemInfoSync();
  174. // #ifdef MP-WEIXIN
  175. this.isPC = /windows/i.test(platform)
  176. this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '2.9.2') >= 0 && !((/ios/i.test(
  177. platform) && /7.0.20/.test(version)) || /wxwork/i.test(environment)) && !this.isPC;
  178. // #endif
  179. // #ifdef MP-TOUTIAO
  180. this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '1.78.0') >= 0;
  181. // #endif
  182. // #ifdef MP-ALIPAY
  183. this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '2.7.0') >= 0;
  184. // #endif
  185. },
  186. // #endif
  187. // #ifdef APP-NVUE
  188. created() {
  189. if (this.hybrid) return
  190. useNvue('_doc/uni_modules/lime-painter/', version, this.timeout).then(res => {
  191. this.isInitFile = true
  192. })
  193. },
  194. // #endif
  195. async mounted() {
  196. await this.getParentWeith()
  197. this.$nextTick(() => {
  198. setTimeout(() => {
  199. if (this.board) {
  200. this.$watch('board', this.watchRender, {
  201. deep: true,
  202. immediate: true
  203. });
  204. } else {
  205. this.$watch('elements', this.watchRender, {
  206. deep: true,
  207. immediate: true
  208. });
  209. }
  210. }, 30)
  211. })
  212. },
  213. methods: {
  214. async watchRender(val, old) {
  215. this.progress = 0
  216. if (!val || !val.views || !val.views.length || JSON.stringify(val) === '{}' || JSON.stringify(val) ==
  217. JSON.stringify(old)) return;
  218. clearTimeout(this.rendertimer)
  219. this.rendertimer = setTimeout(() => {
  220. this.render(val);
  221. }, this.beforeDelay)
  222. },
  223. async setFilePath(path, isEmit) {
  224. let filePath = path
  225. const {
  226. pathType
  227. } = this
  228. if (pathType == 'base64' && !isBase64(path)) {
  229. filePath = await pathToBase64(path)
  230. } else if (pathType == 'url' && isBase64(path)) {
  231. filePath = await base64ToPath(path)
  232. }
  233. if (isEmit) {
  234. this.$emit('success', filePath);
  235. }
  236. return filePath
  237. },
  238. async getSize(args) {
  239. if (!this.size) {
  240. const {
  241. width
  242. } = args.css || args
  243. const {
  244. height
  245. } = args.css || args
  246. if (width || height) {
  247. this.canvasWidth = width || this.canvasWidth
  248. this.canvasHeight = height || this.canvasHeight
  249. await sleep(30);
  250. } else {
  251. await this.getParentWeith()
  252. }
  253. }
  254. },
  255. canvasToTempFilePathSync(args) {
  256. this.$watch('progress', (v) => {
  257. if (v == 1) {
  258. this.canvasToTempFilePath(args)
  259. }
  260. }, {
  261. immediate: true
  262. })
  263. },
  264. // #ifdef APP-NVUE
  265. onPageFinish() {
  266. this.$refs.webview.evalJS(`init(${this.dpr})`)
  267. },
  268. onMessage(e) {
  269. const res = e.detail.data[0] || null;
  270. if (res.event) {
  271. if (res.event == 'inited') {
  272. this.inited = true
  273. }
  274. if (res.event == 'layoutChange') {
  275. const data = JSON.parse(res.data)
  276. this.canvasWidth = data.width;
  277. this.canvasHeight = data.height;
  278. }
  279. if (res.event == 'progressChange') {
  280. this.progress = res.data * 1
  281. }
  282. if (res.event == 'file') {
  283. this.tempFilePath.push(res.data)
  284. if (this.tempFilePath.length > 7) {
  285. this.tempFilePath.shift()
  286. }
  287. return
  288. }
  289. if (res.event == 'success') {
  290. if (res.data) {
  291. this.tempFilePath.push(res.data)
  292. if (this.tempFilePath.length > 8) {
  293. this.tempFilePath.shift()
  294. }
  295. if (this.isCanvasToTempFilePath) {
  296. this.setFilePath(this.tempFilePath.join(''), true)
  297. }
  298. } else {
  299. this.$emit('fail', 'canvas no data')
  300. }
  301. return
  302. }
  303. this.$emit(res.event, JSON.parse(res.data));
  304. } else if (res.file) {
  305. this.file = res.data;
  306. } else if (/ms$/.test(res[0])) {
  307. console.info(res[0])
  308. } else {
  309. this.$emit('fail', res)
  310. }
  311. },
  312. getWebViewInited() {
  313. if (this.inited) return Promise.resolve(this.inited);
  314. return new Promise((resolve) => {
  315. this.$watch(
  316. 'inited',
  317. async val => {
  318. if (val) {
  319. resolve(val)
  320. }
  321. }, {
  322. immediate: true
  323. }
  324. );
  325. })
  326. },
  327. getTempFilePath() {
  328. if (this.tempFilePath.length == 8) return Promise.resolve(this.tempFilePath)
  329. return new Promise((resolve) => {
  330. this.$watch(
  331. 'tempFilePath',
  332. async val => {
  333. if (val.length == 8) {
  334. resolve(val.join(''))
  335. }
  336. }
  337. );
  338. })
  339. },
  340. getWebViewDone() {
  341. if (this.progress == 1) return Promise.resolve(this.progress);
  342. return new Promise((resolve) => {
  343. this.$watch(
  344. 'progress',
  345. async val => {
  346. if (val == 1) {
  347. this.$emit('done')
  348. resolve(val)
  349. }
  350. }, {
  351. immediate: true
  352. }
  353. );
  354. })
  355. },
  356. async render(args) {
  357. try {
  358. await this.getSize(args)
  359. const newNode = await this.calcImage(args);
  360. await this.getWebViewInited()
  361. const webview = this.$refs.webview;
  362. webview.evalJS(`source(${JSON.stringify(newNode)})`)
  363. await this.getWebViewDone()
  364. await sleep(this.afterDelay)
  365. if (this.isCanvasToTempFilePath) {
  366. const params = {
  367. fileType: this.fileType,
  368. quality: this.quality
  369. }
  370. webview.evalJS(`save(${JSON.stringify(params)})`)
  371. }
  372. return Promise.resolve()
  373. } catch (e) {
  374. this.$emit('fail', e)
  375. }
  376. },
  377. async calcImage(args) {
  378. let node = JSON.parse(JSON.stringify(args))
  379. const urlReg = /url\((.+)\)/
  380. const isBG = node.css.backgroundImage && urlReg.exec(node.css.backgroundImage)?. [1]
  381. const url = node.url || node.src || isBG
  382. if ((node.type === "image" || isBG) && url && !isBase64(url)) {
  383. const {
  384. path,
  385. type
  386. } = await getImageInfo(url)
  387. if (isBG) {
  388. node.css.backgroundImage = `url(${path})`
  389. } else {
  390. node.src = path
  391. }
  392. } else if (node.views && node.views.length) {
  393. for (let i = 0; i < node.views.length; i++) {
  394. node.views[i] = await this.calcImage(node.views[i])
  395. }
  396. }
  397. return node
  398. },
  399. async canvasToTempFilePath(args = {}) {
  400. if (!this.inited) {
  401. return this.$emit('fail', 'no init')
  402. }
  403. this.tempFilePath = []
  404. if (args.fileType == 'jpg') {
  405. args.fileType = 'jpeg'
  406. }
  407. this.$refs.webview.evalJS(`save(${JSON.stringify(args)})`)
  408. try {
  409. let tempFilePath = await this.getTempFilePath()
  410. tempFilePath = await this.setFilePath(tempFilePath)
  411. args.success({
  412. errMsg: "canvasToTempFilePath:ok",
  413. tempFilePath
  414. })
  415. } catch (e) {
  416. args.fail({
  417. error: e
  418. })
  419. }
  420. },
  421. // #endif
  422. getParentWeith() {
  423. return new Promise(resolve => {
  424. // #ifdef APP-NVUE
  425. dom.getComponentRect(this.$refs.limepainter, (res) => {
  426. this.canvasWidth = this.canvasWidth || Math.ceil(res.size.width)||300
  427. this.canvasHeight = res.size.height || this.canvasHeight||150
  428. resolve(res.size)
  429. })
  430. // #endif
  431. // #ifndef APP-NVUE
  432. uni.createSelectorQuery()
  433. .in(this)
  434. .select(`.lime-painter`)
  435. .boundingClientRect()
  436. .exec(res => {
  437. this.canvasWidth = Math.ceil(res[0].width)||300
  438. this.canvasHeight = res[0].height || this.canvasHeight||150
  439. resolve(res[0])
  440. })
  441. // #endif
  442. })
  443. },
  444. // #ifndef APP-NVUE
  445. async render(args = {}) {
  446. await this.getSize(args)
  447. const ctx = await this.getContext();
  448. let {
  449. use2dCanvas,
  450. boardWidth,
  451. boardHeight,
  452. canvas,
  453. afterDelay
  454. } = this;
  455. if (use2dCanvas && !canvas) {
  456. return Promise.reject(new Error('render: fail canvas has not been created'));
  457. }
  458. this.boundary = {
  459. top: 0,
  460. left: 0,
  461. width: boardWidth,
  462. height: boardHeight
  463. };
  464. if (!this.painter) {
  465. this.painter = new Painter({
  466. context: ctx,
  467. canvas,
  468. width: boardWidth,
  469. height: boardHeight,
  470. pixelRatio: this.dpr,
  471. fixed: `${this.width?'width':''}${this.height?'height':''}`,
  472. listen: {
  473. onProgress: (v) => {
  474. this.progress = v
  475. this.$emit('progress', v)
  476. },
  477. onEffectFail: (err) => {
  478. this.$emit('faill', err)
  479. }
  480. }
  481. }, this)
  482. }
  483. const {
  484. width,
  485. height
  486. } = await this.painter.source(args)
  487. this.boundary.height = this.canvasHeight = height
  488. this.boundary.width = this.canvasWidth = width
  489. await sleep(this.sleep);
  490. await this.painter.render()
  491. await new Promise(resolve => this.$nextTick(resolve));
  492. if (!use2dCanvas) {
  493. await this.canvasDraw();
  494. }
  495. if (afterDelay && use2dCanvas) {
  496. await sleep(afterDelay);
  497. }
  498. this.$emit('done');
  499. if (this.isCanvasToTempFilePath) {
  500. this.canvasToTempFilePath()
  501. .then(async res => {
  502. this.$emit('success', res.tempFilePath)
  503. })
  504. .catch(err => {
  505. this.$emit('fail', new Error(JSON.stringify(err)));
  506. });
  507. }
  508. return Promise.resolve({
  509. ctx,
  510. draw: this.painter,
  511. node: this.node
  512. });
  513. },
  514. canvasDraw(flag = false) {
  515. return new Promise((resolve, reject) => this.ctx.draw(flag, () => setTimeout(() => resolve(), this
  516. .afterDelay)));
  517. },
  518. async getContext() {
  519. if (!this.canvasWidth) {
  520. this.$emit('fail', 'painter no size')
  521. console.error('painter no size: 请给画板或父级设置尺寸')
  522. return Promise.reject();
  523. }
  524. if (this.ctx && this.inited) {
  525. return Promise.resolve(this.ctx);
  526. }
  527. const {
  528. type,
  529. use2dCanvas,
  530. dpr,
  531. boardWidth,
  532. boardHeight
  533. } = this;
  534. const _getContext = () => {
  535. return new Promise(resolve => {
  536. uni.createSelectorQuery()
  537. .in(this)
  538. .select(`#${this.canvasId}`)
  539. .boundingClientRect()
  540. .exec(res => {
  541. if (res) {
  542. const ctx = uni.createCanvasContext(this.canvasId, this);
  543. if (!this.inited) {
  544. this.inited = true;
  545. this.use2dCanvas = false;
  546. this.canvas = res;
  547. }
  548. if (this.isPC) {
  549. ctx.scale(1 / dpr, 1 / dpr);
  550. }
  551. // #ifdef MP-ALIPAY
  552. ctx.scale(dpr, dpr);
  553. // #endif
  554. this.ctx = ctx
  555. resolve(this.ctx);
  556. }
  557. });
  558. });
  559. };
  560. // #ifndef MP-WEIXIN
  561. return _getContext();
  562. // #endif
  563. if (!use2dCanvas) {
  564. return _getContext();
  565. }
  566. return new Promise(resolve => {
  567. uni.createSelectorQuery()
  568. .in(this)
  569. .select(`#${this.canvasId}`)
  570. .node()
  571. .exec(res => {
  572. let {
  573. node: canvas
  574. } = res[0];
  575. if (!canvas) {
  576. this.use2dCanvas = false;
  577. resolve(this.getContext());
  578. }
  579. const ctx = canvas.getContext(type);
  580. if (!this.inited) {
  581. this.inited = true;
  582. this.use2dCanvas = true;
  583. this.canvas = canvas;
  584. }
  585. this.ctx = ctx
  586. resolve(this.ctx);
  587. });
  588. });
  589. },
  590. canvasToTempFilePath(args = {}) {
  591. const {
  592. use2dCanvas,
  593. canvasId,
  594. dpr,
  595. fileType,
  596. quality
  597. } = this;
  598. return new Promise((resolve, reject) => {
  599. let {
  600. top: y = 0,
  601. left: x = 0,
  602. width,
  603. height
  604. } = this.boundary || this;
  605. let destWidth = width * dpr;
  606. let destHeight = height * dpr;
  607. // #ifdef MP-ALIPAY
  608. width = destWidth;
  609. height = destHeight;
  610. // #endif
  611. const success = async (res) => {
  612. try {
  613. const tempFilePath = await this.setFilePath(res.tempFilePath)
  614. resolve(Object.assign(res, {
  615. tempFilePath
  616. }))
  617. } catch (e) {
  618. this.$emit('fail', e)
  619. }
  620. }
  621. const copyArgs = Object.assign({
  622. x,
  623. y,
  624. width,
  625. height,
  626. destWidth,
  627. destHeight,
  628. canvasId,
  629. fileType,
  630. quality,
  631. success,
  632. fail: reject
  633. }, args);
  634. if (use2dCanvas) {
  635. delete copyArgs.canvasId;
  636. copyArgs.canvas = this.canvas;
  637. }
  638. uni.canvasToTempFilePath(copyArgs, this);
  639. });
  640. }
  641. // #endif
  642. }
  643. };
  644. </script>
  645. <style>
  646. .lime-painter,
  647. .lime-painter__canvas {
  648. // #ifndef APP-NVUE
  649. width: 100%;
  650. // #endif
  651. // #ifdef APP-NVUE
  652. flex: 1;
  653. // #endif
  654. }
  655. </style>