painter.js 26 KB

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