painter.js 32 KB

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