painter.js 26 KB

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