index.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. const main = {
  2. /**
  3. * 渲染块
  4. * @param {Object} params
  5. */
  6. drawBlock({
  7. text,
  8. width = 0,
  9. height,
  10. x,
  11. y,
  12. paddingLeft = 0,
  13. paddingRight = 0,
  14. borderWidth,
  15. backgroundColor,
  16. borderColor,
  17. borderRadius = 0,
  18. opacity = 1,
  19. block,
  20. beyond
  21. }) {
  22. // 判断是否块内有文字
  23. let blockWidth = 0; // 块的宽度
  24. let totalWidth = 0;
  25. let textX = 0;
  26. let textY = 0;
  27. let wid = 750;
  28. if (typeof text !== 'undefined') {
  29. console.log(text)
  30. // 如果有文字并且块的宽度小于文字宽度,块的宽度为 文字的宽度 + 内边距
  31. const textWidth = this._getTextWidth(typeof text.text === 'string' ? text : text.text);
  32. blockWidth = textWidth > width ? textWidth : width;
  33. blockWidth += paddingLeft + paddingLeft;
  34. const {
  35. textAlign = 'left', text: textCon
  36. } = text;
  37. textY = height / 2 + y; // 文字的y轴坐标在块中线
  38. if (textAlign === 'left') {
  39. // 如果是右对齐,那x轴在块的最左边
  40. textX = x + paddingLeft;
  41. } else if (textAlign === 'center') {
  42. textX = blockWidth / 2 + x;
  43. } else {
  44. textX = x + blockWidth - paddingRight;
  45. }
  46. } else {
  47. blockWidth = width;
  48. }
  49. if (backgroundColor) {
  50. // 画面
  51. this.ctx.save();
  52. this.ctx.setGlobalAlpha(opacity);
  53. this.ctx.setFillStyle(backgroundColor);
  54. if (borderRadius > 0) {
  55. // 画圆角矩形
  56. this._drawRadiusRect(x, y, blockWidth, height, borderRadius);
  57. this.ctx.fill();
  58. } else {
  59. this.ctx.fillRect(this.toPx(x), this.toPx(y), this.toPx(blockWidth), this.toPx(height));
  60. }
  61. this.ctx.restore();
  62. }
  63. if (borderWidth) {
  64. // 画线
  65. this.ctx.save();
  66. this.ctx.setGlobalAlpha(opacity);
  67. this.ctx.setStrokeStyle(borderColor);
  68. this.ctx.setLineWidth(this.toPx(borderWidth));
  69. if (borderRadius > 0) {
  70. // 画圆角矩形边框
  71. this._drawRadiusRect(x, y, blockWidth, height, borderRadius);
  72. this.ctx.stroke();
  73. } else {
  74. this.ctx.strokeRect(this.toPx(x), this.toPx(y), this.toPx(blockWidth), this.toPx(height));
  75. }
  76. this.ctx.restore();
  77. }
  78. if (text) {
  79. this.drawText(Object.assign(text, {
  80. x: textX,
  81. y: textY
  82. }))
  83. }
  84. if (block && width) {
  85. let blockWidth = x;
  86. for (let i = 0; i < block.length; i++) {
  87. let val = block[i];
  88. if (blockWidth + (beyond.width || 0) < width) {
  89. this.drawBlock({
  90. ...val,
  91. x: blockWidth
  92. })
  93. } else {
  94. if (beyond) {
  95. this.drawBlock({
  96. ...beyond,
  97. x: blockWidth
  98. })
  99. }
  100. break
  101. }
  102. if (typeof val.text !== 'undefined') {
  103. const textWidth = this._getTextWidth(typeof val.text.text === 'string' ? val.text : val.text.text);
  104. blockWidth += textWidth > (val.width || 0) ? textWidth : (val.width || 0);
  105. blockWidth += val.paddingLeft * 2;
  106. blockWidth += val.marginLeft;
  107. }
  108. }
  109. // block.forEach((val, key) => {
  110. // })
  111. }
  112. },
  113. /**
  114. * 渲染文字
  115. * @param {Object} params
  116. */
  117. drawText(params) {
  118. const {
  119. x,
  120. y,
  121. fontSize,
  122. color,
  123. baseLine,
  124. textAlign,
  125. text,
  126. opacity = 1,
  127. width,
  128. lineNum,
  129. lineHeight
  130. } = params;
  131. if (Object.prototype.toString.call(text) === '[object Array]') {
  132. let preText = {
  133. x,
  134. y,
  135. baseLine
  136. };
  137. text.forEach(item => {
  138. preText.x += item.marginLeft || 0;
  139. const textWidth = this._drawSingleText(Object.assign(item, {
  140. ...preText,
  141. }));
  142. preText.x += textWidth + (item.marginRight || 0); // 下一段字的x轴为上一段字x + 上一段字宽度
  143. })
  144. } else {
  145. this._drawSingleText(params);
  146. }
  147. },
  148. /**
  149. * 渲染图片
  150. */
  151. drawImage(data) {
  152. const {
  153. imgPath,
  154. x,
  155. y,
  156. w,
  157. h,
  158. sx,
  159. sy,
  160. sw,
  161. sh,
  162. borderRadius = 0,
  163. borderWidth = 0,
  164. borderColor
  165. } = data;
  166. this.ctx.save();
  167. if (borderRadius > 0) {
  168. this._drawRadiusRect(x, y, w, h, borderRadius);
  169. this.ctx.clip();
  170. this.ctx.drawImage(imgPath, this.toPx(sx), this.toPx(sy), this.toPx(sw), this.toPx(sh), this.toPx(x), this.toPx(y), this.toPx(w), this.toPx(h));
  171. if (borderWidth > 0) {
  172. this.ctx.setStrokeStyle(borderColor);
  173. this.ctx.setLineWidth(this.toPx(borderWidth));
  174. this.ctx.stroke();
  175. }
  176. } else {
  177. this.ctx.drawImage(imgPath, this.toPx(sx), this.toPx(sy), this.toPx(sw), this.toPx(sh), this.toPx(x), this.toPx(y), this.toPx(w), this.toPx(h));
  178. }
  179. this.ctx.restore();
  180. },
  181. /**
  182. * 渲染线
  183. * @param {*} param0
  184. */
  185. drawLine({
  186. startX,
  187. startY,
  188. endX,
  189. endY,
  190. color,
  191. width
  192. }) {
  193. this.ctx.save();
  194. this.ctx.beginPath();
  195. this.ctx.setStrokeStyle(color);
  196. this.ctx.setLineWidth(this.toPx(width));
  197. this.ctx.moveTo(this.toPx(startX), this.toPx(startY));
  198. this.ctx.lineTo(this.toPx(endX), this.toPx(endY));
  199. this.ctx.stroke();
  200. this.ctx.closePath();
  201. this.ctx.restore();
  202. },
  203. downloadResource(images = []) {
  204. const drawList = [];
  205. this.drawArr = [];
  206. images.forEach((image, index) => drawList.push(this._downloadImageAndInfo(image, index)));
  207. return Promise.all(drawList);
  208. },
  209. initCanvas(w, h, debug) {
  210. return new Promise((resolve) => {
  211. this.setData({
  212. pxWidth: this.toPx(w),
  213. pxHeight: this.toPx(h),
  214. debug,
  215. }, resolve);
  216. });
  217. }
  218. }
  219. const handle = {
  220. /**
  221. * 画圆角矩形
  222. */
  223. _drawRadiusRect(x, y, w, h, r) {
  224. const br = r / 2;
  225. this.ctx.beginPath();
  226. this.ctx.moveTo(this.toPx(x + br), this.toPx(y)); // 移动到左上角的点
  227. this.ctx.lineTo(this.toPx(x + w - br), this.toPx(y));
  228. this.ctx.arc(this.toPx(x + w - br), this.toPx(y + br), this.toPx(br), 2 * Math.PI * (3 / 4), 2 * Math.PI * (4 / 4))
  229. this.ctx.lineTo(this.toPx(x + w), this.toPx(y + h - br));
  230. this.ctx.arc(this.toPx(x + w - br), this.toPx(y + h - br), this.toPx(br), 0, 2 * Math.PI * (1 / 4))
  231. this.ctx.lineTo(this.toPx(x + br), this.toPx(y + h));
  232. this.ctx.arc(this.toPx(x + br), this.toPx(y + h - br), this.toPx(br), 2 * Math.PI * (1 / 4), 2 * Math.PI * (2 / 4))
  233. this.ctx.lineTo(this.toPx(x), this.toPx(y + br));
  234. this.ctx.arc(this.toPx(x + br), this.toPx(y + br), this.toPx(br), 2 * Math.PI * (2 / 4), 2 * Math.PI * (3 / 4))
  235. },
  236. /**
  237. * 计算文本长度
  238. * @param {Array|Object}} text 数组 或者 对象
  239. */
  240. _getTextWidth(text) {
  241. let texts = [];
  242. if (Object.prototype.toString.call(text) === '[object Object]') {
  243. texts.push(text);
  244. } else {
  245. texts = text;
  246. }
  247. let width = 0;
  248. texts.forEach(({
  249. fontSize,
  250. text,
  251. marginLeft = 0,
  252. marginRight = 0
  253. }) => {
  254. this.ctx.setFontSize(this.toPx(fontSize));
  255. width += this.ctx.measureText(text).width + marginLeft + marginRight;
  256. })
  257. return this.toRpx(width);
  258. },
  259. /**
  260. * 渲染一段文字
  261. */
  262. _drawSingleText({
  263. x,
  264. y,
  265. fontSize,
  266. color,
  267. baseLine,
  268. textAlign = 'left',
  269. text,
  270. opacity = 1,
  271. textDecoration = 'none',
  272. width,
  273. lineNum = 1,
  274. lineHeight = 0
  275. }) {
  276. this.ctx.save();
  277. this.ctx.beginPath();
  278. this.ctx.setGlobalAlpha(opacity);
  279. this.ctx.setFontSize(this.toPx(fontSize));
  280. this.ctx.setFillStyle(color);
  281. this.ctx.setTextBaseline(baseLine);
  282. this.ctx.setTextAlign(textAlign);
  283. let textWidth = this.toRpx(this.ctx.measureText(text).width);
  284. const textArr = [];
  285. if (textWidth > width) {
  286. // 文本宽度 大于 渲染宽度
  287. const unitTextWidth = +(textWidth / text.length).toFixed(2);
  288. const unitLineNum = width / unitTextWidth; // 一行文本数量
  289. for (let i = 0; i <= text.length; i += unitLineNum) { // 将文字转为数组,一行文字一个元素
  290. const resText = text.slice(i, i + unitLineNum);
  291. resText !== '' && textArr.push(resText);
  292. if (textArr.length === lineNum) {
  293. break;
  294. }
  295. }
  296. if (textArr.length * unitLineNum < text.length) {
  297. const moreTextWidth = this.ctx.measureText('...').width;
  298. const moreTextNum = Math.ceil(moreTextWidth / unitTextWidth);
  299. const reg = new RegExp(`.{${moreTextNum}}$`);
  300. textArr[textArr.length - 1] = textArr[textArr.length - 1].replace(reg, '...');
  301. }
  302. textWidth = width;
  303. } else {
  304. textArr.push(text);
  305. }
  306. textArr.forEach((item, index) => {
  307. this.ctx.fillText(item, this.toPx(x), this.toPx(y + (lineHeight || fontSize) * index));
  308. })
  309. this.ctx.restore();
  310. // textDecoration
  311. if (textDecoration !== 'none') {
  312. let lineY = y;
  313. if (textDecoration === 'line-through') {
  314. // 目前只支持贯穿线
  315. lineY = y;
  316. }
  317. this.ctx.save();
  318. this.ctx.moveTo(this.toPx(x), this.toPx(lineY));
  319. this.ctx.lineTo(this.toPx(x) + this.toPx(textWidth), this.toPx(lineY));
  320. this.ctx.setStrokeStyle(color);
  321. this.ctx.stroke();
  322. this.ctx.restore();
  323. }
  324. return textWidth;
  325. },
  326. }
  327. const helper = {
  328. /**
  329. * 下载图片并获取图片信息
  330. */
  331. _downloadImageAndInfo(image, index) {
  332. return new Promise((resolve, reject) => {
  333. const {
  334. x,
  335. y,
  336. url,
  337. zIndex
  338. } = image;
  339. const imageUrl = url;
  340. // 下载图片
  341. this._downImage(imageUrl, index)
  342. // 获取图片信息
  343. .then(imgPath => this._getImageInfo(imgPath, index))
  344. .then(({
  345. imgPath,
  346. imgInfo
  347. }) => {
  348. // 根据画布的宽高计算出图片绘制的大小,这里会保证图片绘制不变形
  349. let sx;
  350. let sy;
  351. const borderRadius = image.borderRadius || 0;
  352. const setWidth = image.width;
  353. const setHeight = image.height;
  354. const width = this.toRpx(imgInfo.width);
  355. const height = this.toRpx(imgInfo.height);
  356. if (width / height <= setWidth / setHeight) {
  357. sx = 0;
  358. sy = (height - ((width / setWidth) * setHeight)) / 2;
  359. } else {
  360. sy = 0;
  361. sx = (width - ((height / setHeight) * setWidth)) / 2;
  362. }
  363. this.drawArr.push({
  364. type: 'image',
  365. borderRadius,
  366. borderWidth: image.borderWidth,
  367. borderColor: image.borderColor,
  368. zIndex: typeof zIndex !== 'undefined' ? zIndex : index,
  369. imgPath,
  370. sx,
  371. sy,
  372. sw: (width - (sx * 2)),
  373. sh: (height - (sy * 2)),
  374. x,
  375. y,
  376. w: setWidth,
  377. h: setHeight,
  378. });
  379. resolve();
  380. })
  381. .catch(err => reject(err));
  382. });
  383. },
  384. /**
  385. * 下载图片资源
  386. * @param {*} imageUrl
  387. */
  388. _downImage(imageUrl) {
  389. return new Promise((resolve, reject) => {
  390. if (/^http/.test(imageUrl) && !new RegExp(wx.env.USER_DATA_PATH).test(imageUrl)) {
  391. wx.downloadFile({
  392. url: this._mapHttpToHttps(imageUrl),
  393. success: (res) => {
  394. if (res.statusCode === 200) {
  395. resolve(res.tempFilePath);
  396. } else {
  397. reject(res.errMsg);
  398. }
  399. },
  400. fail(err) {
  401. reject(err);
  402. },
  403. });
  404. } else {
  405. // 支持本地地址
  406. resolve(imageUrl);
  407. }
  408. });
  409. },
  410. /**
  411. * 获取图片信息
  412. * @param {*} imgPath
  413. * @param {*} index
  414. */
  415. _getImageInfo(imgPath, index) {
  416. return new Promise((resolve, reject) => {
  417. wx.getImageInfo({
  418. src: imgPath,
  419. success(res) {
  420. resolve({
  421. imgPath,
  422. imgInfo: res,
  423. index
  424. });
  425. },
  426. fail(err) {
  427. reject(err);
  428. },
  429. });
  430. });
  431. },
  432. toPx(rpx) {
  433. return rpx * this.factor;
  434. },
  435. toRpx(px) {
  436. return px / this.factor;
  437. },
  438. /**
  439. * 将http转为https
  440. * @param {String}} rawUrl 图片资源url
  441. */
  442. _mapHttpToHttps(rawUrl) {
  443. if (rawUrl.indexOf(':') < 0) {
  444. return rawUrl;
  445. }
  446. const urlComponent = rawUrl.split(':');
  447. if (urlComponent.length === 2) {
  448. if (urlComponent[0] === 'http') {
  449. urlComponent[0] = 'https';
  450. return `${urlComponent[0]}:${urlComponent[1]}`;
  451. }
  452. }
  453. return rawUrl;
  454. },
  455. }
  456. Component({
  457. properties: {},
  458. created() {
  459. const sysInfo = wx.getSystemInfoSync();
  460. const screenWidth = sysInfo.screenWidth;
  461. this.factor = screenWidth / 750;
  462. },
  463. methods: Object.assign({
  464. create(config) {
  465. this.ctx = wx.createCanvasContext('canvasid', this);
  466. this.initCanvas(config.width, config.height, config.debug)
  467. .then(() => {
  468. // 设置画布底色
  469. if (config.backgroundColor) {
  470. this.ctx.save();
  471. this.ctx.setFillStyle(config.backgroundColor);
  472. this.ctx.fillRect(0, 0, this.toPx(config.width), this.toPx(config.height));
  473. this.ctx.restore();
  474. }
  475. const {
  476. texts = [], images = [], blocks = [], lines = []
  477. } = config;
  478. const queue = this.drawArr
  479. .concat(texts.map((item) => {
  480. item.type = 'text';
  481. item.zIndex = item.zIndex || 0;
  482. return item;
  483. }))
  484. .concat(blocks.map((item) => {
  485. item.type = 'block';
  486. item.zIndex = item.zIndex || 0;
  487. return item;
  488. }))
  489. .concat(lines.map((item) => {
  490. item.type = 'line';
  491. item.zIndex = item.zIndex || 0;
  492. return item;
  493. }));
  494. // 按照顺序排序
  495. queue.sort((a, b) => a.zIndex - b.zIndex);
  496. queue.forEach((item) => {
  497. if (item.type === 'image') {
  498. this.drawImage(item)
  499. } else if (item.type === 'text') {
  500. this.drawText(item)
  501. } else if (item.type === 'block') {
  502. this.drawBlock(item)
  503. } else if (item.type === 'line') {
  504. this.drawLine(item)
  505. }
  506. });
  507. const res = wx.getSystemInfoSync();
  508. const platform = res.platform;
  509. let time = 0;
  510. if (platform === 'android') {
  511. // 在安卓平台,经测试发现如果海报过于复杂在转换时需要做延时,要不然样式会错乱
  512. time = 300;
  513. }
  514. this.ctx.draw(false, () => {
  515. setTimeout(() => {
  516. wx.canvasToTempFilePath({
  517. canvasId: 'canvasid',
  518. success: (res) => {
  519. this.triggerEvent('success', res.tempFilePath);
  520. },
  521. fail: (err) => {
  522. this.triggerEvent('fail', err);
  523. },
  524. }, this);
  525. }, time);
  526. });
  527. })
  528. .catch((err) => {
  529. wx.showToast({
  530. icon: 'none',
  531. title: err.errMsg || '生成失败'
  532. });
  533. console.error(err);
  534. });
  535. },
  536. }, main, handle, helper),
  537. });