index.js 18 KB

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