index.vue 17 KB

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