Poster.vue 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. <template>
  2. <view class="canvas">
  3. <canvas canvas-id="myCanvas" :style="{width: width+'px',height: height+'px'}"></canvas>
  4. </view>
  5. </template>
  6. <!--
  7. list参数说明:
  8. 图片渲染:
  9. type: 'image',
  10. x: X轴位置,
  11. y: Y轴位置,
  12. path: 图片路径,
  13. width: 图片宽度,
  14. height: 图片高度,
  15. rotate: 旋转角度
  16. shape: 形状,默认无,可选值:circle 圆形
  17. area: {x,y,width,height} // 绘制范围,超出该范围会被剪裁掉 该属性与shape暂时无法同时使用,area存在时,shape失效
  18. 文字渲染:
  19. type: 'text',
  20. x: X轴位置,
  21. y: Y轴位置,
  22. text: 文本内容,
  23. size: 字体大小,
  24. textBaseline: 基线 默认top 可选值:'top'、'bottom'、'middle'、'normal'
  25. color: 颜色
  26. 多行文字渲染:
  27. type: 'textarea',
  28. x: X轴位置,
  29. y: Y轴位置,
  30. width:换行的宽度
  31. height: 高度,溢出会展示“...”
  32. lineSpace: 行间距
  33. text: 文本内容,
  34. size: 字体大小,
  35. textBaseline: 基线 默认top 可选值:'top'、'bottom'、'middle'、'normal'
  36. color: 颜色
  37. -->
  38. <script>
  39. export default {
  40. name: "Poster",
  41. props: {
  42. // 绘制队列
  43. list: {
  44. type: Array,
  45. required: true
  46. },
  47. width: {
  48. type: Number,
  49. required: true
  50. },
  51. height: {
  52. type: Number,
  53. required: true
  54. },
  55. backgroundColor: {
  56. type: String,
  57. default: 'rgba(0,0,0,0)'
  58. }
  59. },
  60. emit: ['on-success', 'on-error'],
  61. data() {
  62. return {
  63. posterUrl: '',
  64. ctx: null, //画布上下文
  65. counter: -1, //计数器
  66. drawPathQueue: [], //画图路径队列
  67. };
  68. },
  69. watch: {
  70. list(newVal, oldVal){
  71. debugger
  72. this.list = newVal
  73. if(this.list.length!=0){
  74. this.generateImg()
  75. console.log('mounted')
  76. }
  77. },
  78. drawPathQueue(newVal, oldVal) {
  79. // 绘制单行文字
  80. const fillText = (textOptions) => {
  81. this.ctx.setFillStyle(textOptions.color)
  82. this.ctx.setFontSize(textOptions.size)
  83. this.ctx.setTextBaseline(textOptions.textBaseline || 'top')
  84. this.ctx.fillText(textOptions.text, textOptions.x, textOptions.y)
  85. }
  86. // 绘制段落
  87. const fillParagraph = (textOptions) => {
  88. this.ctx.setFontSize(textOptions.size)
  89. let tempOptions = JSON.parse(JSON.stringify(textOptions));
  90. // 如果没有指定行间距则设置默认值
  91. tempOptions.lineSpace = tempOptions.lineSpace ? tempOptions.lineSpace : 10;
  92. // 获取字符串
  93. let str = textOptions.text;
  94. // 计算指定高度可以输出的最大行数
  95. let lineCount = Math.floor((tempOptions.height + tempOptions.lineSpace) / (tempOptions.size +
  96. tempOptions.lineSpace))
  97. // 初始化单行宽度
  98. let lineWidth = 0;
  99. let lastSubStrIndex = 0; //每次开始截取的字符串的索引
  100. // 构建一个打印数组
  101. let strArr = str.split("");
  102. let drawArr = [];
  103. let text = "";
  104. while (strArr.length) {
  105. let word = strArr.shift()
  106. text += word;
  107. let textWidth = this.ctx.measureText(text).width;
  108. if (textWidth > textOptions.width) {
  109. // 因为超出宽度 所以要截取掉最后一个字符
  110. text = text.substr(0, text.length - 1)
  111. drawArr.push(text)
  112. text = "";
  113. // 最后一个字还给strArr
  114. strArr.unshift(word)
  115. } else if (!strArr.length) {
  116. drawArr.push(text)
  117. }
  118. }
  119. if (drawArr.length > lineCount) {
  120. // 超出最大行数
  121. drawArr.length = lineCount;
  122. let pointWidth = this.ctx.measureText('...').width;
  123. let wordWidth = 0;
  124. let wordArr = drawArr[drawArr.length - 1].split("");
  125. let words = '';
  126. while (pointWidth > wordWidth) {
  127. words += wordArr.pop();
  128. wordWidth = this.ctx.measureText(words).width
  129. }
  130. drawArr[drawArr.length - 1] = wordArr.join('') + '...';
  131. }
  132. // 打印
  133. for (let i = 0; i < drawArr.length; i++) {
  134. tempOptions.y = tempOptions.y + tempOptions.size * i + tempOptions.lineSpace * i; // y的位置
  135. tempOptions.text = drawArr[i]; // 绘制的文本
  136. fillText(tempOptions)
  137. }
  138. }
  139. // 绘制背景
  140. this.ctx.setFillStyle(this.backgroundColor);
  141. this.ctx.fillRect(0, 0, this.width, this.height);
  142. /* 所有元素入队则开始绘制 */
  143. if (newVal.length === this.list.length) {
  144. try {
  145. // console.log('生成的队列:' + JSON.stringify(newVal));
  146. console.log('开始绘制...')
  147. for (let i = 0; i < this.drawPathQueue.length; i++) {
  148. for (let j = 0; j < this.drawPathQueue.length; j++) {
  149. let current = this.drawPathQueue[j]
  150. /* 按顺序绘制 */
  151. if (current.index === i) {
  152. /* 文本绘制 */
  153. if (current.type === 'text') {
  154. console.log('绘制文本:' + current.text);
  155. fillText(current)
  156. this.counter--
  157. }
  158. /* 多行文本 */
  159. if (current.type === 'textarea') {
  160. console.log('绘制段落:' + current.text);
  161. fillParagraph(current)
  162. this.counter--
  163. }
  164. /* 图片绘制 */
  165. if (current.type === 'image') {
  166. console.log('绘制图片:' + current.path);
  167. if (current.area) {
  168. // 绘制绘图区域
  169. this.ctx.save()
  170. this.ctx.beginPath(); //开始绘制
  171. this.ctx.rect(current.area.x, current.area.y, current.area.width, current.area
  172. .height)
  173. this.ctx.clip();
  174. // 设置旋转中心
  175. let offsetX = current.x + Number(current.width) / 2;
  176. let offsetY = current.y + Number(current.height) / 2;
  177. this.ctx.translate(offsetX, offsetY)
  178. let degrees = current.rotate ? Number(current.rotate) % 360 : 0;
  179. this.ctx.rotate(degrees * Math.PI / 180)
  180. this.ctx.drawImage(current.path, current.x - offsetX, current.y - offsetY,
  181. current.width, current.height)
  182. this.ctx.closePath();
  183. this.ctx.restore(); // 恢复之前保存的上下文
  184. } else if (current.shape == 'circle') {
  185. this.ctx.save(); // 保存上下文,绘制后恢复
  186. this.ctx.beginPath(); //开始绘制
  187. //先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针
  188. let width = (current.width / 2 + current.x);
  189. let height = (current.height / 2 + current.y);
  190. let r = current.width / 2;
  191. this.ctx.arc(width, height, r, 0, Math.PI * 2);
  192. //画好了圆 剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 这也是我们要save上下文的原因
  193. this.ctx.clip();
  194. // 设置旋转中心
  195. let offsetX = current.x + Number(current.width) / 2;
  196. let offsetY = current.y + Number(current.height) / 2;
  197. this.ctx.translate(offsetX, offsetY)
  198. let degrees = current.rotate ? Number(current.rotate) % 360 : 0;
  199. this.ctx.rotate(degrees * Math.PI / 180)
  200. this.ctx.drawImage(current.path, current.x - offsetX, current.y - offsetY,
  201. current.width, current.height)
  202. this.ctx.closePath();
  203. this.ctx.restore(); // 恢复之前保存的上下文
  204. } else {
  205. this.ctx.drawImage(current.path, current.x, current.y, current.width, current
  206. .height)
  207. }
  208. this.counter--
  209. }
  210. }
  211. }
  212. }
  213. } catch (err) {
  214. console.log(err)
  215. this.$emit('on-error', err)
  216. }
  217. }
  218. },
  219. counter(newVal, oldVal) {
  220. if (newVal === 0) {
  221. this.ctx.draw()
  222. /* draw完不能立刻转存,需要等待一段时间 */
  223. setTimeout(() => {
  224. console.log('final counter', this.counter);
  225. uni.canvasToTempFilePath({
  226. canvasId: 'myCanvas',
  227. success: (res) => {
  228. console.log('in canvasToTempFilePath');
  229. // 在H5平台下,tempFilePath 为 base64
  230. // console.log('图片已保存至本地:', res.tempFilePath)
  231. this.posterUrl = res.tempFilePath;
  232. this.$emit('on-success', res.tempFilePath)
  233. },
  234. fail: (res) => {
  235. console.log(res)
  236. }
  237. }, this)
  238. }, 1000)
  239. }
  240. }
  241. },
  242. mounted() {
  243. this.ctx = uni.createCanvasContext('myCanvas', this)
  244. },
  245. methods: {
  246. create() {
  247. this.generateImg()
  248. },
  249. generateImg() {
  250. console.log('generateimg')
  251. this.counter = this.list.length
  252. this.drawPathQueue = []
  253. /* 将图片路径取出放入绘图队列 */
  254. for (let i = 0; i < this.list.length; i++) {
  255. let current = this.list[i]
  256. current.index = i
  257. /* 如果是文本直接放入队列 */
  258. if (current.type === 'text' || current.type === 'textarea') {
  259. this.drawPathQueue.push(current)
  260. continue
  261. }
  262. /* 图片需获取本地缓存path放入队列 */
  263. uni.getImageInfo({
  264. src: current.path,
  265. success: (res) => {
  266. current.path = res.path
  267. this.drawPathQueue.push(current)
  268. }
  269. })
  270. }
  271. },
  272. saveImg() {
  273. uni.canvasToTempFilePath({
  274. canvasId: 'myCanvas',
  275. success: (res) => {
  276. // 在H5平台下,tempFilePath 为 base64
  277. uni.saveImageToPhotosAlbum({
  278. filePath: res.tempFilePath,
  279. success: () => {
  280. console.log('save success');
  281. }
  282. });
  283. }
  284. })
  285. }
  286. }
  287. }
  288. </script>
  289. <style lang="scss" scoped>
  290. .canvas {
  291. position: fixed;
  292. top: 100rpx;
  293. left: 750rpx;
  294. }
  295. </style>