painter.vue 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912
  1. <template>
  2. <uni-shadow-root class="painter-painter"><view :style="'position: relative;'+(customStyle)+';'+(painterStyle)">
  3. <block v-if="(!use2D)">
  4. <canvas canvas-id="photo" :style="(photoStyle)+';position: absolute; left: -9999px; top: -9999rpx;'"></canvas>
  5. <canvas canvas-id="bottom" :style="(painterStyle)+';position: absolute;'"></canvas>
  6. <canvas canvas-id="k-canvas" :style="(painterStyle)+';position: absolute;'"></canvas>
  7. <canvas canvas-id="top" :style="(painterStyle)+';position: absolute;'"></canvas>
  8. <canvas canvas-id="front" :style="(painterStyle)+';position: absolute;'" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" @touchcancel="onTouchCancel" :disable-scroll="true"></canvas>
  9. </block>
  10. <block v-if="use2D">
  11. <canvas type="2d" id="photo" :style="(photoStyle)+';'"></canvas>
  12. </block>
  13. </view></uni-shadow-root>
  14. </template>
  15. <script>
  16. global['__wxVueOptions'] = {components:{}}
  17. global['__wxRoute'] = 'painter/painter'
  18. import Pen from './lib/pen';
  19. import Downloader from './lib/downloader';
  20. import WxCanvas from './lib/wx-canvas';
  21. const util = require('./lib/util');
  22. const downloader = new Downloader();
  23. // 最大尝试的绘制次数
  24. const MAX_PAINT_COUNT = 5;
  25. const ACTION_DEFAULT_SIZE = 24;
  26. const ACTION_OFFSET = '2rpx';
  27. Component({
  28. canvasWidthInPx: 0,
  29. canvasHeightInPx: 0,
  30. canvasNode: null,
  31. paintCount: 0,
  32. currentPalette: {},
  33. movingCache: {},
  34. outterDisabled: false,
  35. isDisabled: false,
  36. needClear: false,
  37. properties: {
  38. use2D: {
  39. type: Boolean,
  40. },
  41. customStyle: {
  42. type: String,
  43. },
  44. // 运行自定义选择框和删除缩放按钮
  45. customActionStyle: {
  46. type: Object,
  47. },
  48. palette: {
  49. type: Object,
  50. observer: function (newVal, oldVal) {
  51. if (this.isNeedRefresh(newVal, oldVal)) {
  52. this.paintCount = 0;
  53. this.startPaint();
  54. }
  55. },
  56. },
  57. dancePalette: {
  58. type: Object,
  59. observer: function (newVal, oldVal) {
  60. if (!this.isEmpty(newVal) && !this.properties.use2D) {
  61. this.initDancePalette(newVal);
  62. }
  63. },
  64. },
  65. // 缩放比,会在传入的 palette 中统一乘以该缩放比
  66. scaleRatio: {
  67. type: Number,
  68. value: 1
  69. },
  70. widthPixels: {
  71. type: Number,
  72. value: 0
  73. },
  74. // 启用脏检查,默认 false
  75. dirty: {
  76. type: Boolean,
  77. value: false,
  78. },
  79. LRU: {
  80. type: Boolean,
  81. value: false,
  82. },
  83. action: {
  84. type: Object,
  85. observer: function (newVal, oldVal) {
  86. if (newVal && !this.isEmpty(newVal) && !this.properties.use2D) {
  87. this.doAction(newVal, (callbackInfo) => {
  88. this.movingCache = callbackInfo
  89. }, false, true)
  90. }
  91. },
  92. },
  93. disableAction: {
  94. type: Boolean,
  95. observer: function (isDisabled) {
  96. this.outterDisabled = isDisabled
  97. this.isDisabled = isDisabled
  98. }
  99. },
  100. clearActionBox: {
  101. type: Boolean,
  102. observer: function (needClear) {
  103. if (needClear && !this.needClear) {
  104. if (this.frontContext) {
  105. setTimeout(() => {
  106. this.frontContext.draw();
  107. }, 100);
  108. this.touchedView = {};
  109. this.prevFindedIndex = this.findedIndex
  110. this.findedIndex = -1;
  111. }
  112. }
  113. this.needClear = needClear
  114. }
  115. },
  116. },
  117. data: {
  118. picURL: '',
  119. showCanvas: true,
  120. painterStyle: '',
  121. },
  122. methods: {
  123. /**
  124. * 判断一个 object 是否为 空
  125. * @param {object} object
  126. */
  127. isEmpty(object) {
  128. for (const i in object) {
  129. return false;
  130. }
  131. return true;
  132. },
  133. isNeedRefresh(newVal, oldVal) {
  134. if (!newVal || this.isEmpty(newVal) || (this.data.dirty && util.equal(newVal, oldVal))) {
  135. return false;
  136. }
  137. return true;
  138. },
  139. getBox(rect, type) {
  140. const boxArea = {
  141. type: 'rect',
  142. css: {
  143. height: `${rect.bottom - rect.top}px`,
  144. width: `${rect.right - rect.left}px`,
  145. left: `${rect.left}px`,
  146. top: `${rect.top}px`,
  147. borderWidth: '4rpx',
  148. borderColor: '#1A7AF8',
  149. color: 'transparent'
  150. }
  151. }
  152. if (type === 'text') {
  153. boxArea.css = Object.assign({}, boxArea.css, {
  154. borderStyle: 'dashed'
  155. })
  156. }
  157. if (this.properties.customActionStyle && this.properties.customActionStyle.border) {
  158. boxArea.css = Object.assign({}, boxArea.css, this.properties.customActionStyle.border)
  159. }
  160. Object.assign(boxArea, {
  161. id: 'box'
  162. })
  163. return boxArea
  164. },
  165. getScaleIcon(rect, type) {
  166. let scaleArea = {}
  167. const {
  168. customActionStyle
  169. } = this.properties
  170. if (customActionStyle && customActionStyle.scale) {
  171. scaleArea = {
  172. type: 'image',
  173. url: type === 'text' ? customActionStyle.scale.textIcon : customActionStyle.scale.imageIcon,
  174. css: {
  175. height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
  176. width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
  177. borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
  178. }
  179. }
  180. } else {
  181. scaleArea = {
  182. type: 'rect',
  183. css: {
  184. height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
  185. width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
  186. borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
  187. color: '#0000ff',
  188. }
  189. }
  190. }
  191. scaleArea.css = Object.assign({}, scaleArea.css, {
  192. align: 'center',
  193. left: `${rect.right + ACTION_OFFSET.toPx()}px`,
  194. top: type === 'text' ? `${rect.top - ACTION_OFFSET.toPx() - scaleArea.css.height.toPx() / 2}px` : `${rect.bottom - ACTION_OFFSET.toPx() - scaleArea.css.height.toPx() / 2}px`
  195. })
  196. Object.assign(scaleArea, {
  197. id: 'scale'
  198. })
  199. return scaleArea
  200. },
  201. getDeleteIcon(rect) {
  202. let deleteArea = {}
  203. const {
  204. customActionStyle
  205. } = this.properties
  206. if (customActionStyle && customActionStyle.scale) {
  207. deleteArea = {
  208. type: 'image',
  209. url: customActionStyle.delete.icon,
  210. css: {
  211. height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
  212. width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
  213. borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
  214. }
  215. }
  216. } else {
  217. deleteArea = {
  218. type: 'rect',
  219. css: {
  220. height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
  221. width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
  222. borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
  223. color: '#0000ff',
  224. }
  225. }
  226. }
  227. deleteArea.css = Object.assign({}, deleteArea.css, {
  228. align: 'center',
  229. left: `${rect.left - ACTION_OFFSET.toPx()}px`,
  230. top: `${rect.top - ACTION_OFFSET.toPx() - deleteArea.css.height.toPx() / 2}px`
  231. })
  232. Object.assign(deleteArea, {
  233. id: 'delete'
  234. })
  235. return deleteArea
  236. },
  237. doAction(action, callback, isMoving, overwrite) {
  238. if (this.properties.use2D) {
  239. return;
  240. }
  241. let newVal = null
  242. if (action) {
  243. newVal = action.view
  244. }
  245. if (newVal && newVal.id && this.touchedView.id !== newVal.id) {
  246. // 带 id 的动作给撤回时使用,不带 id,表示对当前选中对象进行操作
  247. const {
  248. views
  249. } = this.currentPalette;
  250. for (let i = 0; i < views.length; i++) {
  251. if (views[i].id === newVal.id) {
  252. // 跨层回撤,需要重新构建三层关系
  253. this.touchedView = views[i];
  254. this.findedIndex = i;
  255. this.sliceLayers();
  256. break
  257. }
  258. }
  259. }
  260. const doView = this.touchedView
  261. if (!doView || this.isEmpty(doView)) {
  262. return
  263. }
  264. if (newVal && newVal.css) {
  265. if (overwrite) {
  266. doView.css = newVal.css
  267. } else if (Array.isArray(doView.css) && Array.isArray(newVal.css)) {
  268. doView.css = Object.assign({}, ...doView.css, ...newVal.css)
  269. } else if (Array.isArray(doView.css)) {
  270. doView.css = Object.assign({}, ...doView.css, newVal.css)
  271. } else if (Array.isArray(newVal.css)) {
  272. doView.css = Object.assign({}, doView.css, ...newVal.css)
  273. } else {
  274. doView.css = Object.assign({}, doView.css, newVal.css)
  275. }
  276. }
  277. if (newVal && newVal.rect) {
  278. doView.rect = newVal.rect;
  279. }
  280. if (newVal && newVal.url && doView.url && newVal.url !== doView.url) {
  281. downloader.download(newVal.url, this.properties.LRU).then((path) => {
  282. if (newVal.url.startsWith('https')) {
  283. doView.originUrl = newVal.url
  284. }
  285. doView.url = path;
  286. wx.getImageInfo({
  287. src: path,
  288. success: (res) => {
  289. doView.sHeight = res.height
  290. doView.sWidth = res.width
  291. this.reDraw(doView, callback, isMoving)
  292. },
  293. fail: () => {
  294. this.reDraw(doView, callback, isMoving)
  295. }
  296. })
  297. }).catch((error) => {
  298. // 未下载成功,直接绘制
  299. console.error(error)
  300. this.reDraw(doView, callback, isMoving)
  301. })
  302. } else {
  303. (newVal && newVal.text && doView.text && newVal.text !== doView.text) && (doView.text = newVal.text);
  304. (newVal && newVal.content && doView.content && newVal.content !== doView.content) && (doView.content = newVal.content);
  305. this.reDraw(doView, callback, isMoving)
  306. }
  307. },
  308. reDraw(doView, callback, isMoving) {
  309. const draw = {
  310. width: this.currentPalette.width,
  311. height: this.currentPalette.height,
  312. views: this.isEmpty(doView) ? [] : [doView]
  313. }
  314. const pen = new Pen(this.globalContext, draw);
  315. if (isMoving && doView.type === 'text') {
  316. pen.paint((callbackInfo) => {
  317. callback && callback(callbackInfo);
  318. this.triggerEvent('viewUpdate', {
  319. view: this.touchedView
  320. });
  321. }, true, this.movingCache);
  322. } else {
  323. // 某些机型(华为 P20)非移动和缩放场景下,只绘制一遍会偶然性图片绘制失败
  324. // if (!isMoving && !this.isScale) {
  325. // pen.paint()
  326. // }
  327. pen.paint((callbackInfo) => {
  328. callback && callback(callbackInfo);
  329. this.triggerEvent('viewUpdate', {
  330. view: this.touchedView
  331. });
  332. })
  333. }
  334. const {
  335. rect,
  336. css,
  337. type
  338. } = doView
  339. this.block = {
  340. width: this.currentPalette.width,
  341. height: this.currentPalette.height,
  342. views: this.isEmpty(doView) ? [] : [this.getBox(rect, doView.type)]
  343. }
  344. if (css && css.scalable) {
  345. this.block.views.push(this.getScaleIcon(rect, type))
  346. }
  347. if (css && css.deletable) {
  348. this.block.views.push(this.getDeleteIcon(rect))
  349. }
  350. const topBlock = new Pen(this.frontContext, this.block)
  351. topBlock.paint();
  352. },
  353. isInView(x, y, rect) {
  354. return (x > rect.left &&
  355. y > rect.top &&
  356. x < rect.right &&
  357. y < rect.bottom
  358. )
  359. },
  360. isInDelete(x, y) {
  361. for (const view of this.block.views) {
  362. if (view.id === 'delete') {
  363. return (x > view.rect.left &&
  364. y > view.rect.top &&
  365. x < view.rect.right &&
  366. y < view.rect.bottom)
  367. }
  368. }
  369. return false
  370. },
  371. isInScale(x, y) {
  372. for (const view of this.block.views) {
  373. if (view.id === 'scale') {
  374. return (x > view.rect.left &&
  375. y > view.rect.top &&
  376. x < view.rect.right &&
  377. y < view.rect.bottom)
  378. }
  379. }
  380. return false
  381. },
  382. touchedView: {},
  383. findedIndex: -1,
  384. onClick() {
  385. const x = this.startX
  386. const y = this.startY
  387. const totalLayerCount = this.currentPalette.views.length
  388. let canBeTouched = []
  389. let isDelete = false
  390. let deleteIndex = -1
  391. for (let i = totalLayerCount - 1; i >= 0; i--) {
  392. const view = this.currentPalette.views[i]
  393. const {
  394. rect
  395. } = view
  396. if (this.touchedView && this.touchedView.id && this.touchedView.id === view.id && this.isInDelete(x, y, rect)) {
  397. canBeTouched.length = 0
  398. deleteIndex = i
  399. isDelete = true
  400. break
  401. }
  402. if (this.isInView(x, y, rect)) {
  403. canBeTouched.push({
  404. view,
  405. index: i
  406. })
  407. }
  408. }
  409. this.touchedView = {}
  410. if (canBeTouched.length === 0) {
  411. this.findedIndex = -1
  412. } else {
  413. let i = 0
  414. const touchAble = canBeTouched.filter(item => Boolean(item.view.id))
  415. if (touchAble.length === 0) {
  416. this.findedIndex = canBeTouched[0].index
  417. } else {
  418. for (i = 0; i < touchAble.length; i++) {
  419. if (this.findedIndex === touchAble[i].index) {
  420. i++
  421. break
  422. }
  423. }
  424. if (i === touchAble.length) {
  425. i = 0
  426. }
  427. this.touchedView = touchAble[i].view
  428. this.findedIndex = touchAble[i].index
  429. this.triggerEvent('viewClicked', {
  430. view: this.touchedView
  431. })
  432. }
  433. }
  434. if (this.findedIndex < 0 || (this.touchedView && !this.touchedView.id)) {
  435. // 证明点击了背景 或无法移动的view
  436. this.frontContext.draw();
  437. if (isDelete) {
  438. this.triggerEvent('touchEnd', {
  439. view: this.currentPalette.views[deleteIndex],
  440. index: deleteIndex,
  441. type: 'delete'
  442. })
  443. this.doAction()
  444. } else if (this.findedIndex < 0) {
  445. this.triggerEvent('viewClicked', {})
  446. }
  447. this.findedIndex = -1
  448. this.prevFindedIndex = -1
  449. } else if (this.touchedView && this.touchedView.id) {
  450. this.sliceLayers();
  451. }
  452. },
  453. sliceLayers() {
  454. const bottomLayers = this.currentPalette.views.slice(0, this.findedIndex)
  455. const topLayers = this.currentPalette.views.slice(this.findedIndex + 1)
  456. const bottomDraw = {
  457. width: this.currentPalette.width,
  458. height: this.currentPalette.height,
  459. background: this.currentPalette.background,
  460. views: bottomLayers
  461. }
  462. const topDraw = {
  463. width: this.currentPalette.width,
  464. height: this.currentPalette.height,
  465. views: topLayers
  466. }
  467. if (this.prevFindedIndex < this.findedIndex) {
  468. new Pen(this.bottomContext, bottomDraw).paint();
  469. this.doAction(null, (callbackInfo) => {
  470. this.movingCache = callbackInfo
  471. })
  472. new Pen(this.topContext, topDraw).paint();
  473. } else {
  474. new Pen(this.topContext, topDraw).paint();
  475. this.doAction(null, (callbackInfo) => {
  476. this.movingCache = callbackInfo
  477. })
  478. new Pen(this.bottomContext, bottomDraw).paint();
  479. }
  480. this.prevFindedIndex = this.findedIndex
  481. },
  482. startX: 0,
  483. startY: 0,
  484. startH: 0,
  485. startW: 0,
  486. isScale: false,
  487. startTimeStamp: 0,
  488. onTouchStart(event) {
  489. if (this.isDisabled) {
  490. return
  491. }
  492. const {
  493. x,
  494. y
  495. } = event.touches[0]
  496. this.startX = x
  497. this.startY = y
  498. this.startTimeStamp = new Date().getTime()
  499. if (this.touchedView && !this.isEmpty(this.touchedView)) {
  500. const {
  501. rect
  502. } = this.touchedView
  503. if (this.isInScale(x, y, rect)) {
  504. this.isScale = true
  505. this.movingCache = {}
  506. this.startH = rect.bottom - rect.top
  507. this.startW = rect.right - rect.left
  508. } else {
  509. this.isScale = false
  510. }
  511. } else {
  512. this.isScale = false
  513. }
  514. },
  515. onTouchEnd(e) {
  516. if (this.isDisabled) {
  517. return
  518. }
  519. const current = new Date().getTime()
  520. if ((current - this.startTimeStamp) <= 500 && !this.hasMove) {
  521. !this.isScale && this.onClick(e)
  522. } else if (this.touchedView && !this.isEmpty(this.touchedView)) {
  523. this.triggerEvent('touchEnd', {
  524. view: this.touchedView,
  525. })
  526. }
  527. this.hasMove = false
  528. },
  529. onTouchCancel(e) {
  530. if (this.isDisabled) {
  531. return
  532. }
  533. this.onTouchEnd(e)
  534. },
  535. hasMove: false,
  536. onTouchMove(event) {
  537. if (this.isDisabled) {
  538. return
  539. }
  540. this.hasMove = true
  541. if (!this.touchedView || (this.touchedView && !this.touchedView.id)) {
  542. return
  543. }
  544. const {
  545. x,
  546. y
  547. } = event.touches[0]
  548. const offsetX = x - this.startX
  549. const offsetY = y - this.startY
  550. const {
  551. rect,
  552. type
  553. } = this.touchedView
  554. let css = {}
  555. if (this.isScale) {
  556. const newW = this.startW + offsetX > 1 ? this.startW + offsetX : 1
  557. if (this.touchedView.css && this.touchedView.css.minWidth) {
  558. if (newW < this.touchedView.css.minWidth.toPx()) {
  559. return
  560. }
  561. }
  562. if (this.touchedView.rect && this.touchedView.rect.minWidth) {
  563. if (newW < this.touchedView.rect.minWidth) {
  564. return
  565. }
  566. }
  567. const newH = this.startH + offsetY > 1 ? this.startH + offsetY : 1
  568. css = {
  569. width: `${newW}px`,
  570. }
  571. if (type !== 'text') {
  572. if (type === 'image') {
  573. css.height = `${(newW) * this.startH / this.startW}px`
  574. } else {
  575. css.height = `${newH}px`
  576. }
  577. }
  578. } else {
  579. this.startX = x
  580. this.startY = y
  581. css = {
  582. left: `${rect.x + offsetX}px`,
  583. top: `${rect.y + offsetY}px`,
  584. right: undefined,
  585. bottom: undefined
  586. }
  587. }
  588. this.doAction({
  589. view: {
  590. css
  591. }
  592. }, (callbackInfo) => {
  593. if (this.isScale) {
  594. this.movingCache = callbackInfo
  595. }
  596. }, !this.isScale)
  597. },
  598. initScreenK() {
  599. if (!(getApp() && getApp().systemInfo && getApp().systemInfo.screenWidth)) {
  600. try {
  601. getApp().systemInfo = wx.getSystemInfoSync();
  602. } catch (e) {
  603. console.error(`Painter get system info failed, ${JSON.stringify(e)}`);
  604. return;
  605. }
  606. }
  607. this.screenK = 0.5;
  608. if (getApp() && getApp().systemInfo && getApp().systemInfo.screenWidth) {
  609. this.screenK = getApp().systemInfo.screenWidth / 750;
  610. }
  611. setStringPrototype(this.screenK, this.properties.scaleRatio);
  612. },
  613. initDancePalette() {
  614. if (this.properties.use2D) {
  615. return;
  616. }
  617. this.isDisabled = true;
  618. this.initScreenK();
  619. this.downloadImages(this.properties.dancePalette).then(async (palette) => {
  620. this.currentPalette = palette
  621. const {
  622. width,
  623. height
  624. } = palette;
  625. if (!width || !height) {
  626. console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`);
  627. return;
  628. }
  629. this.setData({
  630. painterStyle: `width:${width.toPx()}px;height:${height.toPx()}px;`,
  631. });
  632. this.frontContext || (this.frontContext = await this.getCanvasContext(this.properties.use2D, 'front'));
  633. this.bottomContext || (this.bottomContext = await this.getCanvasContext(this.properties.use2D, 'bottom'));
  634. this.topContext || (this.topContext = await this.getCanvasContext(this.properties.use2D, 'top'));
  635. this.globalContext || (this.globalContext = await this.getCanvasContext(this.properties.use2D, 'k-canvas'));
  636. new Pen(this.bottomContext, palette, this.properties.use2D).paint(() => {
  637. this.isDisabled = false;
  638. this.isDisabled = this.outterDisabled;
  639. this.triggerEvent('didShow');
  640. });
  641. this.globalContext.draw();
  642. this.frontContext.draw();
  643. this.topContext.draw();
  644. });
  645. this.touchedView = {};
  646. },
  647. startPaint() {
  648. this.initScreenK();
  649. this.downloadImages(this.properties.palette).then(async (palette) => {
  650. const {
  651. width,
  652. height
  653. } = palette;
  654. if (!width || !height) {
  655. console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`);
  656. return;
  657. }
  658. let needScale = false;
  659. // 生成图片时,根据设置的像素值重新绘制
  660. if (width.toPx() !== this.canvasWidthInPx) {
  661. this.canvasWidthInPx = width.toPx();
  662. needScale = this.properties.use2D;
  663. }
  664. if (this.properties.widthPixels) {
  665. setStringPrototype(this.screenK, this.properties.widthPixels / this.canvasWidthInPx)
  666. this.canvasWidthInPx = this.properties.widthPixels
  667. }
  668. if (this.canvasHeightInPx !== height.toPx()) {
  669. this.canvasHeightInPx = height.toPx();
  670. needScale = needScale || this.properties.use2D;
  671. }
  672. if (!this.photoContext) {
  673. this.photoContext = await this.getCanvasContext(this.properties.use2D, 'photo');
  674. }
  675. if (needScale) {
  676. const scale = getApp().systemInfo.pixelRatio;
  677. this.photoContext.width = this.canvasWidthInPx * scale;
  678. this.photoContext.height = this.canvasHeightInPx * scale;
  679. this.photoContext.scale(scale, scale);
  680. }
  681. this.setData({
  682. photoStyle: `width:${this.canvasWidthInPx}px;height:${this.canvasHeightInPx}px;`,
  683. }, () => {
  684. new Pen(this.photoContext, palette).paint(() => {
  685. this.saveImgToLocal();
  686. });
  687. setStringPrototype(this.screenK, this.properties.scaleRatio);
  688. });
  689. });
  690. },
  691. downloadImages(palette) {
  692. return new Promise((resolve, reject) => {
  693. let preCount = 0;
  694. let completeCount = 0;
  695. const paletteCopy = JSON.parse(JSON.stringify(palette));
  696. if (paletteCopy.background) {
  697. preCount++;
  698. downloader.download(paletteCopy.background, this.properties.LRU).then((path) => {
  699. paletteCopy.background = path;
  700. completeCount++;
  701. if (preCount === completeCount) {
  702. resolve(paletteCopy);
  703. }
  704. }, () => {
  705. completeCount++;
  706. if (preCount === completeCount) {
  707. resolve(paletteCopy);
  708. }
  709. });
  710. }
  711. if (paletteCopy.views) {
  712. for (const view of paletteCopy.views) {
  713. if (view && view.type === 'image' && view.url) {
  714. preCount++;
  715. /* eslint-disable no-loop-func */
  716. downloader.download(view.url, this.properties.LRU).then((path) => {
  717. view.originUrl = view.url;
  718. view.url = path;
  719. wx.getImageInfo({
  720. src: path,
  721. success: (res) => {
  722. // 获得一下图片信息,供后续裁减使用
  723. view.sWidth = res.width;
  724. view.sHeight = res.height;
  725. },
  726. fail: (error) => {
  727. // 如果图片坏了,则直接置空,防止坑爹的 canvas 画崩溃了
  728. console.warn(`getImageInfo ${view.originUrl} failed, ${JSON.stringify(error)}`);
  729. view.url = "";
  730. },
  731. complete: () => {
  732. completeCount++;
  733. if (preCount === completeCount) {
  734. resolve(paletteCopy);
  735. }
  736. },
  737. });
  738. }, () => {
  739. completeCount++;
  740. if (preCount === completeCount) {
  741. resolve(paletteCopy);
  742. }
  743. });
  744. }
  745. }
  746. }
  747. if (preCount === 0) {
  748. resolve(paletteCopy);
  749. }
  750. });
  751. },
  752. saveImgToLocal() {
  753. const that = this;
  754. setTimeout(() => {
  755. wx.canvasToTempFilePath({
  756. canvasId: 'photo',
  757. canvas: that.properties.use2D ? that.canvasNode : null,
  758. destWidth: that.canvasWidthInPx,
  759. destHeight: that.canvasHeightInPx,
  760. success: function (res) {
  761. that.getImageInfo(res.tempFilePath);
  762. },
  763. fail: function (error) {
  764. console.error(`canvasToTempFilePath failed, ${JSON.stringify(error)}`);
  765. that.triggerEvent('imgErr', {
  766. error: error
  767. });
  768. },
  769. }, this);
  770. }, 300);
  771. },
  772. getCanvasContext(use2D, id) {
  773. const that = this;
  774. return new Promise(resolve => {
  775. if (use2D) {
  776. const query = wx.createSelectorQuery().in(that);
  777. const selectId = `#${id}`;
  778. query.select(selectId)
  779. .fields({node: true, size: true})
  780. .exec((res) => {
  781. that.canvasNode = res[0].node;
  782. const ctx = that.canvasNode.getContext('2d');
  783. const wxCanvas = new WxCanvas('2d', ctx, id, true, that.canvasNode);
  784. resolve(wxCanvas);
  785. });
  786. } else {
  787. const temp = wx.createCanvasContext(id, that);
  788. resolve(new WxCanvas('mina', temp, id, true));
  789. }
  790. })
  791. },
  792. getImageInfo(filePath) {
  793. const that = this;
  794. wx.getImageInfo({
  795. src: filePath,
  796. success: (infoRes) => {
  797. if (that.paintCount > MAX_PAINT_COUNT) {
  798. const error = `The result is always fault, even we tried ${MAX_PAINT_COUNT} times`;
  799. console.error(error);
  800. that.triggerEvent('imgErr', {
  801. error: error
  802. });
  803. return;
  804. }
  805. // 比例相符时才证明绘制成功,否则进行强制重绘制
  806. if (Math.abs((infoRes.width * that.canvasHeightInPx - that.canvasWidthInPx * infoRes.height) / (infoRes.height * that.canvasHeightInPx)) < 0.01) {
  807. that.triggerEvent('imgOK', {
  808. path: filePath
  809. });
  810. } else {
  811. that.startPaint();
  812. }
  813. that.paintCount++;
  814. },
  815. fail: (error) => {
  816. console.error(`getImageInfo failed, ${JSON.stringify(error)}`);
  817. that.triggerEvent('imgErr', {
  818. error: error
  819. });
  820. },
  821. });
  822. },
  823. },
  824. });
  825. function setStringPrototype(screenK, scale) {
  826. /* eslint-disable no-extend-native */
  827. /**
  828. * 是否支持负数
  829. * @param {Boolean} minus 是否支持负数
  830. * @param {Number} baseSize 当设置了 % 号时,设置的基准值
  831. */
  832. String.prototype.toPx = function toPx(minus, baseSize) {
  833. if (this === '0') {
  834. return 0
  835. }
  836. let reg;
  837. if (minus) {
  838. reg = /^-?[0-9]+([.]{1}[0-9]+){0,1}(rpx|px|%)$/g;
  839. } else {
  840. reg = /^[0-9]+([.]{1}[0-9]+){0,1}(rpx|px|%)$/g;
  841. }
  842. const results = reg.exec(this);
  843. if (!this || !results) {
  844. console.error(`The size: ${this} is illegal`);
  845. return 0;
  846. }
  847. const unit = results[2];
  848. const value = parseFloat(this);
  849. let res = 0;
  850. if (unit === 'rpx') {
  851. res = Math.round(value * (screenK || 0.5) * (scale || 1));
  852. } else if (unit === 'px') {
  853. res = Math.round(value * (scale || 1));
  854. } else if (unit === '%') {
  855. res = Math.round(value * baseSize / 100);
  856. }
  857. return res;
  858. };
  859. }
  860. export default global['__wxComponents']['painter/painter']
  861. </script>
  862. <style platform="mp-weixin">
  863. </style>