Poster.vue 9.9 KB

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