├── LICENSE ├── README.md ├── app.js ├── app.json ├── app.wxss ├── components └── canvasdrawer │ ├── canvasdrawer.js │ ├── canvasdrawer.json │ ├── canvasdrawer.wxml │ └── canvasdrawer.wxss ├── images ├── avatar.jpeg ├── avatar_cover.jpeg ├── background.jpeg ├── pic.jpeg └── wxacode.jpeg ├── pages ├── index │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss └── multiple │ ├── multiple.js │ ├── multiple.json │ ├── multiple.wxml │ └── multiple.wxss └── project.config.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Di 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## canvas drawer 2 | 3 | 新增 [mpvue_canvas_drawer](https://github.com/kuckboy1994/mpvue_canvas_drawer)。之后同步更新。 4 | 5 | 新增由[simmzl](https://github.com/simmzl)开发移植的[wepy_canvas_drawer](https://github.com/simmzl/wepy_canvas_drawer)。之后同步更新。 6 | 7 | 做微信小程序中最好用的 `canvas` 绘图组件之一。 8 | 9 | 当前环境下,大家都非常需要分享到朋友圈这个功能,但是实现起来各有心酸(坑比较多),所以才有了如下的 `canvas` 绘图工具。 10 | 11 | 具有如下特性: 12 | 13 | - 简单易用 —— 一个 `json` 搞定绘制图片 14 | - 功能全 —— 满足 `90%` 的使用场景 15 | - 绘制文本(换行、超出内容省略号、中划线、下划线、文本加粗) 16 | - 绘制图片 17 | - 绘制矩形 18 | - 保存图片 19 | - 多图绘制 20 | - ... 21 | - 代码量小 22 | 23 | ## 体验 24 | 25 | ``` 26 | git clone https://github.com/kuckboy1994/mp_canvas_drawer 27 | ``` 28 | 想在手机上使用配置自己的 `appid` 即可。 29 | 30 | 编译模式中已经为你配置好比较常用的两种模式: 31 | - 普通绘制,绘制单张分享图。 32 | - 多图绘制,连续绘制分享图 33 | 34 | ## 演示 35 | 36 | ![](http://wx3.sinaimg.cn/mw690/ec4d7780gy1ft7eihf9f0g206u0bnhdw.gif) 37 | 38 | 左侧是 `canvasdrawer` 绘制的,右侧是UI给的图 39 | 40 | ![](http://wx2.sinaimg.cn/mw690/ec4d7780gy1ft7h2qezidj21140qw1kx.jpg) 41 | 42 | ## 使用 43 | 44 | - `git clone https://github.com/kuckboy1994/mp_canvas_drawer` 到本地 45 | - 把 `components` 中的 `canvasdrawer` 拷贝到自己项目下。 46 | - 在使用页面注册组件 47 | ```json 48 | { 49 | "usingComponents": { 50 | "canvasdrawer": "/components/canvasdrawer/canvasdrawer" 51 | } 52 | } 53 | ``` 54 | - 在页面 `**.wxml` 文件中加入如下代码 55 | ```html 56 | 57 | ``` 58 | `painting` 是需要传入的 `json`。 `getImage` 方法是绘图完成之后的回调函数,在 `event.detail` 中返回绘制完成的图片地址。 59 | - 当前栗子中的 `painting` 简单展示一下。详细配置请看 [API](https://github.com/kuckboy1994/mp_canvas_drawer#api) 60 | 61 | 62 |
painting(点击展开)
63 | 64 | ```js 65 | { 66 | width: 375, 67 | height: 555, 68 | views: [ 69 | { 70 | type: 'image', 71 | url: 'https://hybrid.xiaoying.tv/miniprogram/viva-ad/1/1531103986231.jpeg', 72 | top: 0, 73 | left: 0, 74 | width: 375, 75 | height: 555 76 | }, 77 | { 78 | type: 'image', 79 | url: 'https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83epJEPdPqQVgv6D8bojGT4DrGXuEC4Oe0GXs5sMsN4GGpCegTUsBgL9SPJkN9UqC1s0iakjQpwd4h4A/132', 80 | top: 27.5, 81 | left: 29, 82 | width: 55, 83 | height: 55 84 | }, 85 | { 86 | type: 'image', 87 | url: 'https://hybrid.xiaoying.tv/miniprogram/viva-ad/1/1531401349117.jpeg', 88 | top: 27.5, 89 | left: 29, 90 | width: 55, 91 | height: 55 92 | }, 93 | { 94 | type: 'text', 95 | content: '您的好友【kuckboy】', 96 | fontSize: 16, 97 | color: '#402D16', 98 | textAlign: 'left', 99 | top: 33, 100 | left: 96, 101 | bolder: true 102 | }, 103 | { 104 | type: 'text', 105 | content: '发现一件好货,邀请你一起0元免费拿!', 106 | fontSize: 15, 107 | color: '#563D20', 108 | textAlign: 'left', 109 | top: 59.5, 110 | left: 96 111 | }, 112 | { 113 | type: 'image', 114 | url: 'https://hybrid.xiaoying.tv/miniprogram/viva-ad/1/1531385366950.jpeg', 115 | top: 136, 116 | left: 42.5, 117 | width: 290, 118 | height: 186 119 | }, 120 | { 121 | type: 'image', 122 | url: 'https://hybrid.xiaoying.tv/miniprogram/viva-ad/1/1531385433625.jpeg', 123 | top: 443, 124 | left: 85, 125 | width: 68, 126 | height: 68 127 | }, 128 | { 129 | type: 'text', 130 | content: '正品MAC魅可口红礼盒生日唇膏小辣椒Chili西柚情人', 131 | fontSize: 16, 132 | lineHeight: 21, 133 | color: '#383549', 134 | textAlign: 'left', 135 | top: 336, 136 | left: 44, 137 | width: 287, 138 | MaxLineNumber: 2, 139 | breakWord: true, 140 | bolder: true 141 | }, 142 | { 143 | type: 'text', 144 | content: '¥0.00', 145 | fontSize: 19, 146 | color: '#E62004', 147 | textAlign: 'left', 148 | top: 387, 149 | left: 44.5, 150 | bolder: true 151 | }, 152 | { 153 | type: 'text', 154 | content: '原价:¥138.00', 155 | fontSize: 13, 156 | color: '#7E7E8B', 157 | textAlign: 'left', 158 | top: 391, 159 | left: 110, 160 | textDecoration: 'line-through' 161 | }, 162 | { 163 | type: 'text', 164 | content: '长按识别图中二维码帮我砍个价呗~', 165 | fontSize: 14, 166 | color: '#383549', 167 | textAlign: 'left', 168 | top: 460, 169 | left: 165.5, 170 | lineHeight: 20, 171 | MaxLineNumber: 2, 172 | breakWord: true, 173 | width: 125 174 | } 175 | ] 176 | } 177 | ``` 178 |
179 | 180 | ## API 181 | 182 |
对象结构一览
183 | 184 | ```js 185 | { 186 | width: 375, 187 | height: 555, 188 | views: [ 189 | { 190 | type: 'image', 191 | url: 'url', 192 | top: 0, 193 | left: 0, 194 | width: 375, 195 | height: 555 196 | }, 197 | { 198 | type: 'text', 199 | content: 'content', 200 | fontSize: 16, 201 | color: '#402D16', 202 | textAlign: 'left', 203 | top: 33, 204 | left: 96, 205 | bolder: true 206 | }, 207 | { 208 | type: 'rect', 209 | background: 'color', 210 | top: 0, 211 | left: 0, 212 | width: 375, 213 | height: 555 214 | } 215 | ] 216 | } 217 | ``` 218 |
219 | 220 | 221 | 数据对象的第一层需要三个参数: `width`、`height`、`mode`、`views`。配置中所有的数字都是没有单位的。这就意味着 `canvas` 绘制的是一个比例图。具体显示的大小直接把返回的图片路径放置到 `image` 标签中即可。 222 | 223 | `mode` 可选值有 `same`, 默认值为空,常规下尽量不要使用。如要使用请看 Q&A的第1点。 224 | 225 | 当前可以绘制3种类型的配置: `image`、`text`、`rect`。配置的属性基本上使用的都是 `css` 的驼峰名称,还是比较好理解的。 226 | 227 | ### image(图片) 228 | 属性 | 含义 | 默认值 | 可选值 229 | ---|---|---|--- 230 | url | 绘制的图片地址,可以是本地图片,如:`/images/1.jpeg` | | 231 | top | 左上角距离画板顶部的距离 | | 232 | left | 左上角距离画板左侧的距离 | | 233 | width | 要画多宽 | 0 | 234 | height | 要画多高 | 0 | 235 | 236 | ### text(文本) 237 | 属性 | 含义 | 默认值 | 可选值 238 | ---|---|---|--- 239 | content | 绘制文本 | ''(空字符串) | 240 | color | 颜色 | black | 241 | fontSize | 字体大小 | 16 | 242 | textAlign | 文字对齐方式 | left | center、right 243 | lineHeight | 行高,只有在多行文本中才有用 | 20 | 244 | top | 文本左上角距离画板顶部的距离 | 0 | 245 | left | 文本左上角距离画板左侧的距离 | 0 | 246 | breakWord | 是否需要换行 | false | true 247 | MaxLineNumber | 最大行数,只有设置 `breakWord: true` ,当前属性才有效,超出行数内容的显示为... | 2 | 248 | width | 和 `MaxLineNumber` 属性配套使用,`width` 就是达到换行的宽度 | | 249 | bolder | 是否加粗 | false | true 250 | textDecoration | 显示中划线、下划线效果 | none | underline(下划线)、line-through(中划线) 251 | 252 | ### rect (矩形,线条) 253 | 属性 | 含义 | 默认值 | 可选值 254 | ---|---|---|--- 255 | background | 背景颜色 | black | 256 | top | 左上角距离画板顶部的距离 | | 257 | left | 左上角距离画板左侧的距离 | | 258 | width | 要画多宽 | 0 | 259 | height | 要画多高 | 0 | 260 | 261 | ## Q&A 262 | 0. 最佳实践 263 | 264 | 绘制操作的时候最好 `锁住屏幕` ,例如在点击绘制的时候 265 | ```js 266 | wx.showLoading({ 267 | title: '绘制分享图片中', 268 | mask: true 269 | }) 270 | ``` 271 | 绘制完成之后 272 | ```js 273 | wx.hideLoading() 274 | ``` 275 | 具体可以参考项目下的 `/pages/multiple` 276 | 1. [mpvue] 由于 `canvasdrawer` 不主动呈现绘制内容,而是交给调用者去使用 `image` 来展示,所以在mpvue更新数据就会render整个组件的,之后 `canvasdrawer` 又会重新被渲染,导致无限循环,所以默认情况下我把代码改为,传入的 `painting` 和之前的一样的话,组件就不渲染了。只有出现差异的内容才会更新(触发回调),这种个人认为还是可以接受的。 277 | 增加顶层参数 `mode`, `mode: 'same'` 为可以绘制同样的内容。在 `mpvue` 模式下`勿用`。 278 | 2. 二维码和小程序码如何绘制? 279 | - 二维码和小程序码可以通过调用[微信官方的接口](https://developers.weixin.qq.com/miniprogram/dev/api/qrcode.html)产生,需要后端配合。 280 | - 然后走 `type: image` 类型进行绘制即可。 281 | 3. 绘制流程相关 282 | - `views` 数组中的顺序代表绘画的先后顺序,会有覆盖的现象。请各位使用者注意。 283 | 4. 如何实现圆形头像? 284 | - 由于完成一些效果,例如: `文字下划线` 等。必须要使用微信小程序 `rect` 相关的接口,和 `clip` 接口感觉相处的不好(存在bug)。可以查看 [微信小程序社区的帖子](https://developers.weixin.qq.com/blogdetail?action=get_post_info&docid=00086255ef09d0df4b0751f6651000&highline=clip&token=895863755&lang=zh_CN)。 285 | - so,提供一种解决方式:使用一张中间镂空的图片盖在头像上。 286 | 5. `canvas drawer` 组件为什么不直接显示canvas画板和其内容呢? 287 | - 考虑到大部分场景,我们都是用来把图片保存到本地,或用以展示。 288 | - 保存到本地,返回临时文件给调用者一定是最佳的解决方式。 289 | - 展示,转化成图片之后,就可以使用 `image` 基础组件的所有显示模式了,还能设置宽高。 290 | 291 | ## 更新计划 292 | 293 | - [x] 图片缓存机制 - 加快相同图片绘制的速度 294 | - [x] 增加 `measureText` 方法对于低版本的提示 295 | - [x] 安卓下文本绘制稳定性修复 296 | - [ ] 增加圆角属性 `borderRadius`、`border` 支持 297 | - [x] 错误异常回调支持 298 | - [ ] 预缓存模式 299 | - [ ] 优化 `measureText` 测量模式 300 | - [x] mpvue 版本小程序 301 | 302 | ## TIPS 303 | 304 | 如果有什么疑问,欢迎 `issues`。 如果觉得不错,能不能送我小 ✨ ✨ ,让我有更多的动力。 305 | 306 | 307 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | //app.js 2 | App({}) -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages":[ 3 | "pages/index/index", 4 | "pages/multiple/multiple" 5 | ], 6 | "window":{ 7 | "backgroundTextStyle":"light", 8 | "navigationBarBackgroundColor": "#fff", 9 | "navigationBarTitleText": "WeChat", 10 | "navigationBarTextStyle":"black" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app.wxss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuckboy1994/mp_canvas_drawer/18e2274a058b57bd962fd199a0b06d2628dcfa1f/app.wxss -------------------------------------------------------------------------------- /components/canvasdrawer/canvasdrawer.js: -------------------------------------------------------------------------------- 1 | /* global Component wx */ 2 | 3 | Component({ 4 | properties: { 5 | painting: { 6 | type: Object, 7 | value: {view: []}, 8 | observer (newVal, oldVal) { 9 | if (!this.data.isPainting) { 10 | if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) { 11 | if (newVal && newVal.width && newVal.height) { 12 | this.setData({ 13 | showCanvas: true, 14 | isPainting: true 15 | }) 16 | this.readyPigment() 17 | } 18 | } else { 19 | if (newVal && newVal.mode !== 'same') { 20 | this.triggerEvent('getImage', {errMsg: 'canvasdrawer:samme params'}) 21 | } 22 | } 23 | } 24 | } 25 | } 26 | }, 27 | data: { 28 | showCanvas: false, 29 | 30 | width: 100, 31 | height: 100, 32 | 33 | tempFileList: [], 34 | 35 | isPainting: false 36 | }, 37 | ctx: null, 38 | cache: {}, 39 | ready () { 40 | wx.removeStorageSync('canvasdrawer_pic_cache') 41 | this.cache = wx.getStorageSync('canvasdrawer_pic_cache') || {} 42 | this.ctx = wx.createCanvasContext('canvasdrawer', this) 43 | }, 44 | methods: { 45 | readyPigment () { 46 | const { width, height, views } = this.data.painting 47 | this.setData({ 48 | width, 49 | height 50 | }) 51 | 52 | const inter = setInterval(() => { 53 | if (this.ctx) { 54 | clearInterval(inter) 55 | this.ctx.clearActions() 56 | this.ctx.save() 57 | this.getImagesInfo(views) 58 | } 59 | }, 100) 60 | }, 61 | getImagesInfo (views) { 62 | const imageList = [] 63 | for (let i = 0; i < views.length; i++) { 64 | if (views[i].type === 'image') { 65 | imageList.push(this.getImageInfo(views[i].url)) 66 | } 67 | } 68 | 69 | const loadTask = [] 70 | for (let i = 0; i < Math.ceil(imageList.length / 8); i++) { 71 | loadTask.push(new Promise((resolve, reject) => { 72 | Promise.all(imageList.splice(i * 8, 8)).then(res => { 73 | resolve(res) 74 | }).catch(res => { 75 | reject(res) 76 | }) 77 | })) 78 | } 79 | Promise.all(loadTask).then(res => { 80 | let tempFileList = [] 81 | for (let i = 0; i < res.length; i++) { 82 | tempFileList = tempFileList.concat(res[i]) 83 | } 84 | this.setData({ 85 | tempFileList 86 | }) 87 | this.startPainting() 88 | }) 89 | }, 90 | startPainting () { 91 | const { tempFileList, painting: { views } } = this.data 92 | console.log(tempFileList) 93 | for (let i = 0, imageIndex = 0; i < views.length; i++) { 94 | if (views[i].type === 'image') { 95 | this.drawImage({ 96 | ...views[i], 97 | url: tempFileList[imageIndex] 98 | }) 99 | imageIndex++ 100 | } else if (views[i].type === 'text') { 101 | if (!this.ctx.measureText) { 102 | wx.showModal({ 103 | title: '提示', 104 | content: '当前微信版本过低,无法使用 measureText 功能,请升级到最新微信版本后重试。' 105 | }) 106 | this.triggerEvent('getImage', {errMsg: 'canvasdrawer:version too low'}) 107 | return 108 | } else { 109 | this.drawText(views[i]) 110 | } 111 | } else if (views[i].type === 'rect') { 112 | this.drawRect(views[i]) 113 | } 114 | } 115 | this.ctx.draw(false, () => { 116 | wx.setStorageSync('canvasdrawer_pic_cache', this.cache) 117 | const system = wx.getSystemInfoSync().system 118 | if (/ios/i.test(system)) { 119 | this.saveImageToLocal() 120 | } else { 121 | // 延迟保存图片,解决安卓生成图片错位bug。 122 | setTimeout(() => { 123 | this.saveImageToLocal() 124 | }, 800) 125 | } 126 | }) 127 | }, 128 | drawImage (params) { 129 | this.ctx.save() 130 | const { url, top = 0, left = 0, width = 0, height = 0, borderRadius = 0, deg = 0 } = params 131 | // if (borderRadius) { 132 | // this.ctx.beginPath() 133 | // this.ctx.arc(left + borderRadius, top + borderRadius, borderRadius, 0, 2 * Math.PI) 134 | // this.ctx.clip() 135 | // this.ctx.drawImage(url, left, top, width, height) 136 | // } else { 137 | if (deg !== 0) { 138 | this.ctx.translate(left + width/2, top + height/2) 139 | this.ctx.rotate(deg * Math.PI / 180) 140 | this.ctx.drawImage(url, -width/2, -height/2, width, height) 141 | } else { 142 | this.ctx.drawImage(url, left, top, width, height) 143 | } 144 | // } 145 | this.ctx.restore() 146 | }, 147 | drawText (params) { 148 | this.ctx.save() 149 | const { 150 | MaxLineNumber = 2, 151 | breakWord = false, 152 | color = 'black', 153 | content = '', 154 | fontSize = 16, 155 | top = 0, 156 | left = 0, 157 | lineHeight = 20, 158 | textAlign = 'left', 159 | width, 160 | bolder = false, 161 | textDecoration = 'none' 162 | } = params 163 | 164 | this.ctx.beginPath() 165 | this.ctx.setTextBaseline('top') 166 | this.ctx.setTextAlign(textAlign) 167 | this.ctx.setFillStyle(color) 168 | this.ctx.setFontSize(fontSize) 169 | 170 | if (!breakWord) { 171 | this.ctx.fillText(content, left, top) 172 | this.drawTextLine(left, top, textDecoration, color, fontSize, content) 173 | } else { 174 | let fillText = '' 175 | let fillTop = top 176 | let lineNum = 1 177 | for (let i = 0; i < content.length; i++) { 178 | fillText += [content[i]] 179 | if (this.ctx.measureText(fillText).width > width) { 180 | if (lineNum === MaxLineNumber) { 181 | if (i !== content.length) { 182 | fillText = fillText.substring(0, fillText.length - 1) + '...' 183 | this.ctx.fillText(fillText, left, fillTop) 184 | this.drawTextLine(left, fillTop, textDecoration, color, fontSize, fillText) 185 | fillText = '' 186 | break 187 | } 188 | } 189 | this.ctx.fillText(fillText, left, fillTop) 190 | this.drawTextLine(left, fillTop, textDecoration, color, fontSize, fillText) 191 | fillText = '' 192 | fillTop += lineHeight 193 | lineNum ++ 194 | } 195 | } 196 | this.ctx.fillText(fillText, left, fillTop) 197 | this.drawTextLine(left, fillTop, textDecoration, color, fontSize, fillText) 198 | } 199 | 200 | this.ctx.restore() 201 | 202 | if (bolder) { 203 | this.drawText({ 204 | ...params, 205 | left: left + 0.3, 206 | top: top + 0.3, 207 | bolder: false, 208 | textDecoration: 'none' 209 | }) 210 | } 211 | }, 212 | drawTextLine (left, top, textDecoration, color, fontSize, content) { 213 | if (textDecoration === 'underline') { 214 | this.drawRect({ 215 | background: color, 216 | top: top + fontSize * 1.2, 217 | left: left - 1, 218 | width: this.ctx.measureText(content).width + 3, 219 | height: 1 220 | }) 221 | } else if (textDecoration === 'line-through') { 222 | this.drawRect({ 223 | background: color, 224 | top: top + fontSize * 0.6, 225 | left: left - 1, 226 | width: this.ctx.measureText(content).width + 3, 227 | height: 1 228 | }) 229 | } 230 | }, 231 | drawRect (params) { 232 | this.ctx.save() 233 | const { background, top = 0, left = 0, width = 0, height = 0 } = params 234 | this.ctx.setFillStyle(background) 235 | this.ctx.fillRect(left, top, width, height) 236 | this.ctx.restore() 237 | }, 238 | getImageInfo (url) { 239 | return new Promise((resolve, reject) => { 240 | if (this.cache[url]) { 241 | resolve(this.cache[url]) 242 | } else { 243 | const objExp = new RegExp(/^http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/) 244 | if (objExp.test(url)) { 245 | wx.getImageInfo({ 246 | src: url, 247 | complete: res => { 248 | if (res.errMsg === 'getImageInfo:ok') { 249 | this.cache[url] = res.path 250 | resolve(res.path) 251 | } else { 252 | this.triggerEvent('getImage', {errMsg: 'canvasdrawer:download fail'}) 253 | reject(new Error('getImageInfo fail')) 254 | } 255 | } 256 | }) 257 | } else { 258 | this.cache[url] = url 259 | resolve(url) 260 | } 261 | } 262 | }) 263 | }, 264 | saveImageToLocal () { 265 | const { width, height } = this.data 266 | wx.canvasToTempFilePath({ 267 | x: 0, 268 | y: 0, 269 | width, 270 | height, 271 | canvasId: 'canvasdrawer', 272 | complete: res => { 273 | if (res.errMsg === 'canvasToTempFilePath:ok') { 274 | this.setData({ 275 | showCanvas: false, 276 | isPainting: false, 277 | tempFileList: [] 278 | }) 279 | this.triggerEvent('getImage', {tempFilePath: res.tempFilePath, errMsg: 'canvasdrawer:ok'}) 280 | } else { 281 | this.triggerEvent('getImage', {errMsg: 'canvasdrawer:fail'}) 282 | } 283 | } 284 | }, this) 285 | } 286 | } 287 | }) 288 | -------------------------------------------------------------------------------- /components/canvasdrawer/canvasdrawer.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true 3 | } -------------------------------------------------------------------------------- /components/canvasdrawer/canvasdrawer.wxml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/canvasdrawer/canvasdrawer.wxss: -------------------------------------------------------------------------------- 1 | .board { 2 | position: fixed; 3 | top: 2000rpx; 4 | } -------------------------------------------------------------------------------- /images/avatar.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuckboy1994/mp_canvas_drawer/18e2274a058b57bd962fd199a0b06d2628dcfa1f/images/avatar.jpeg -------------------------------------------------------------------------------- /images/avatar_cover.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuckboy1994/mp_canvas_drawer/18e2274a058b57bd962fd199a0b06d2628dcfa1f/images/avatar_cover.jpeg -------------------------------------------------------------------------------- /images/background.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuckboy1994/mp_canvas_drawer/18e2274a058b57bd962fd199a0b06d2628dcfa1f/images/background.jpeg -------------------------------------------------------------------------------- /images/pic.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuckboy1994/mp_canvas_drawer/18e2274a058b57bd962fd199a0b06d2628dcfa1f/images/pic.jpeg -------------------------------------------------------------------------------- /images/wxacode.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuckboy1994/mp_canvas_drawer/18e2274a058b57bd962fd199a0b06d2628dcfa1f/images/wxacode.jpeg -------------------------------------------------------------------------------- /pages/index/index.js: -------------------------------------------------------------------------------- 1 | //index.js 2 | 3 | Page({ 4 | data: { 5 | painting: {}, 6 | shareImage: '' 7 | }, 8 | onLoad () { 9 | // this.eventDraw() 10 | }, 11 | eventDraw () { 12 | wx.showLoading({ 13 | title: '绘制分享图片中', 14 | mask: true 15 | }) 16 | this.setData({ 17 | painting: { 18 | width: 375, 19 | height: 555, 20 | clear: true, 21 | views: [ 22 | { 23 | type: 'image', 24 | url: 'https://hybrid.xiaoying.tv/miniprogram/viva-ad/1/1531103986231.jpeg', 25 | top: 0, 26 | left: 0, 27 | width: 375, 28 | height: 555 29 | }, 30 | { 31 | type: 'image', 32 | url: 'https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83epJEPdPqQVgv6D8bojGT4DrGXuEC4Oe0GXs5sMsN4GGpCegTUsBgL9SPJkN9UqC1s0iakjQpwd4h4A/132', 33 | top: 27.5, 34 | left: 29, 35 | width: 55, 36 | height: 55 37 | }, 38 | { 39 | type: 'image', 40 | url: 'https://hybrid.xiaoying.tv/miniprogram/viva-ad/1/1531401349117.jpeg', 41 | top: 27.5, 42 | left: 29, 43 | width: 55, 44 | height: 55 45 | }, 46 | { 47 | type: 'text', 48 | content: '您的好友【kuckboy】', 49 | fontSize: 16, 50 | color: '#402D16', 51 | textAlign: 'left', 52 | top: 33, 53 | left: 96, 54 | bolder: true 55 | }, 56 | { 57 | type: 'text', 58 | content: '发现一件好货,邀请你一起0元免费拿!', 59 | fontSize: 15, 60 | color: '#563D20', 61 | textAlign: 'left', 62 | top: 59.5, 63 | left: 96 64 | }, 65 | { 66 | type: 'image', 67 | url: 'https://hybrid.xiaoying.tv/miniprogram/viva-ad/1/1531385366950.jpeg', 68 | top: 136, 69 | left: 42.5, 70 | width: 290, 71 | height: 186 72 | }, 73 | { 74 | type: 'image', 75 | url: 'https://hybrid.xiaoying.tv/miniprogram/viva-ad/1/1531385433625.jpeg', 76 | top: 443, 77 | left: 85, 78 | width: 68, 79 | height: 68 80 | }, 81 | { 82 | type: 'text', 83 | content: '正品MAC魅可口红礼盒生日唇膏小辣椒Chili西柚情人', 84 | fontSize: 16, 85 | lineHeight: 21, 86 | color: '#383549', 87 | textAlign: 'left', 88 | top: 336, 89 | left: 44, 90 | width: 287, 91 | MaxLineNumber: 2, 92 | breakWord: true, 93 | bolder: true 94 | }, 95 | { 96 | type: 'text', 97 | content: '¥0.00', 98 | fontSize: 19, 99 | color: '#E62004', 100 | textAlign: 'left', 101 | top: 387, 102 | left: 44.5, 103 | bolder: true 104 | }, 105 | { 106 | type: 'text', 107 | content: '原价:¥138.00', 108 | fontSize: 13, 109 | color: '#7E7E8B', 110 | textAlign: 'left', 111 | top: 391, 112 | left: 110, 113 | textDecoration: 'line-through' 114 | }, 115 | { 116 | type: 'text', 117 | content: '长按识别图中二维码帮我砍个价呗~', 118 | fontSize: 14, 119 | color: '#383549', 120 | textAlign: 'left', 121 | top: 460, 122 | left: 165.5, 123 | lineHeight: 20, 124 | MaxLineNumber: 2, 125 | breakWord: true, 126 | width: 125 127 | } 128 | ] 129 | } 130 | }) 131 | }, 132 | eventSave () { 133 | wx.saveImageToPhotosAlbum({ 134 | filePath: this.data.shareImage, 135 | success (res) { 136 | wx.showToast({ 137 | title: '保存图片成功', 138 | icon: 'success', 139 | duration: 2000 140 | }) 141 | } 142 | }) 143 | }, 144 | eventGetImage (event) { 145 | console.log(event) 146 | wx.hideLoading() 147 | const { tempFilePath, errMsg } = event.detail 148 | if (errMsg === 'canvasdrawer:ok') { 149 | this.setData({ 150 | shareImage: tempFilePath 151 | }) 152 | } 153 | } 154 | }) 155 | -------------------------------------------------------------------------------- /pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "canvas drawer", 3 | "usingComponents": { 4 | "canvasdrawer": "/components/canvasdrawer/canvasdrawer" 5 | } 6 | } -------------------------------------------------------------------------------- /pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | .share-image { 2 | width: 600rpx; 3 | height: 888rpx; 4 | margin: 0 75rpx; 5 | border: 1px solid black; 6 | } 7 | 8 | button { 9 | margin-top: 20rpx; 10 | } -------------------------------------------------------------------------------- /pages/multiple/multiple.js: -------------------------------------------------------------------------------- 1 | //index.js 2 | //获取应用实例 3 | const app = getApp() 4 | 5 | Page({ 6 | data: { 7 | painting: {}, 8 | paintingIndex: 0, 9 | paintingList: [ 10 | { 11 | width: 375, 12 | height: 555, 13 | clear: true, 14 | views: [ 15 | { 16 | type: 'image', 17 | url: 'https://hybrid.xiaoying.tv/miniprogram/viva-ad/1/1531103986231.jpeg', 18 | top: 0, 19 | left: 0, 20 | width: 375, 21 | height: 555 22 | }, 23 | { 24 | type: 'image', 25 | url: 'https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83epJEPdPqQVgv6D8bojGT4DrGXuEC4Oe0GXs5sMsN4GGpCegTUsBgL9SPJkN9UqC1s0iakjQpwd4h4A/132', 26 | top: 27.5, 27 | left: 29, 28 | width: 55, 29 | height: 55 30 | }, 31 | { 32 | type: 'image', 33 | url: 'https://hybrid.xiaoying.tv/miniprogram/viva-ad/1/1531401349117.jpeg', 34 | top: 27.5, 35 | left: 29, 36 | width: 55, 37 | height: 55 38 | }, 39 | { 40 | type: 'text', 41 | content: '您的好友【kuckboy】', 42 | fontSize: 16, 43 | color: '#402D16', 44 | textAlign: 'left', 45 | top: 33, 46 | left: 96, 47 | bolder: true 48 | }, 49 | { 50 | type: 'text', 51 | content: '发现一件好货,邀请你一起0元免费拿!', 52 | fontSize: 15, 53 | color: '#563D20', 54 | textAlign: 'left', 55 | top: 59.5, 56 | left: 96 57 | }, 58 | { 59 | type: 'image', 60 | url: 'https://hybrid.xiaoying.tv/miniprogram/viva-ad/1/1531385366950.jpeg', 61 | top: 136, 62 | left: 42.5, 63 | width: 290, 64 | height: 186 65 | }, 66 | { 67 | type: 'image', 68 | url: 'https://hybrid.xiaoying.tv/miniprogram/viva-ad/1/1531385433625.jpeg', 69 | top: 443, 70 | left: 85, 71 | width: 68, 72 | height: 68 73 | }, 74 | { 75 | type: 'text', 76 | content: '正品MAC魅可口红礼盒生日唇膏小辣椒Chili西柚情人', 77 | fontSize: 16, 78 | lineHeight: 21, 79 | color: '#383549', 80 | textAlign: 'left', 81 | top: 336, 82 | left: 44, 83 | width: 287, 84 | MaxLineNumber: 2, 85 | breakWord: true, 86 | bolder: true 87 | }, 88 | { 89 | type: 'text', 90 | content: '¥0.00', 91 | fontSize: 19, 92 | color: '#E62004', 93 | textAlign: 'left', 94 | top: 387, 95 | left: 44.5, 96 | bolder: true 97 | }, 98 | { 99 | type: 'text', 100 | content: '原价:¥138.00', 101 | fontSize: 13, 102 | color: '#7E7E8B', 103 | textAlign: 'left', 104 | top: 391, 105 | left: 110, 106 | textDecoration: 'line-through' 107 | }, 108 | { 109 | type: 'text', 110 | content: '长按识别图中二维码帮我砍个价呗~', 111 | fontSize: 14, 112 | color: '#383549', 113 | textAlign: 'left', 114 | top: 460, 115 | left: 165.5, 116 | lineHeight: 20, 117 | MaxLineNumber: 2, 118 | breakWord: true, 119 | width: 125 120 | } 121 | ] 122 | }, 123 | { 124 | width: 375, 125 | height: 555, 126 | clear: true, 127 | views: [ 128 | { 129 | type: 'image', 130 | url: 'https://hybrid.xiaoying.tv/miniprogram/viva-ad/1/1531447237017.jpeg', 131 | top: 0, 132 | left: 0, 133 | width: 375, 134 | height: 555 135 | } 136 | ] 137 | } 138 | ], 139 | shareImage: '', 140 | 141 | mode: 'normal' // cry 142 | }, 143 | eventDraw () { 144 | wx.showLoading({ 145 | title: '绘制分享图片中', 146 | mask: true 147 | }) 148 | const { paintingList, paintingIndex } = this.data 149 | this.setData({ 150 | mode: 'normal', 151 | painting: paintingList[paintingIndex], 152 | paintingIndex: paintingIndex === 0 ? 1 : 0 153 | }) 154 | }, 155 | eventSave () { 156 | wx.saveImageToPhotosAlbum({ 157 | filePath: this.data.shareImage, 158 | success (res) { 159 | wx.showToast({ 160 | title: '保存图片成功', 161 | icon: 'success', 162 | duration: 2000 163 | }) 164 | } 165 | }) 166 | }, 167 | eventGetImage (event) { 168 | wx.hideLoading() 169 | const { tempFilePath } = event.detail 170 | this.setData({ 171 | shareImage: tempFilePath 172 | }) 173 | if (this.data.mode === 'cry') { 174 | this.eventDrawCry() 175 | } 176 | }, 177 | eventDrawCry () { 178 | wx.showLoading({ 179 | title: '刷新后停止绘制', 180 | mask: true 181 | }) 182 | const { paintingList, paintingIndex } = this.data 183 | this.setData({ 184 | mode: 'cry', 185 | painting: paintingList[paintingIndex], 186 | paintingIndex: paintingIndex === 0 ? 1 : 0 187 | }) 188 | } 189 | }) 190 | -------------------------------------------------------------------------------- /pages/multiple/multiple.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "canvas drawer 多图绘制", 3 | "usingComponents": { 4 | "canvasdrawer": "/components/canvasdrawer/canvasdrawer" 5 | } 6 | } -------------------------------------------------------------------------------- /pages/multiple/multiple.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /pages/multiple/multiple.wxss: -------------------------------------------------------------------------------- 1 | .share-image { 2 | width: 600rpx; 3 | height: 888rpx; 4 | margin: 0 75rpx; 5 | border: 1px solid black; 6 | } 7 | 8 | button { 9 | margin-top: 20rpx; 10 | } -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件。", 3 | "packOptions": { 4 | "ignore": [] 5 | }, 6 | "setting": { 7 | "urlCheck": false, 8 | "es6": true, 9 | "postcss": true, 10 | "minified": true, 11 | "newFeature": true 12 | }, 13 | "compileType": "miniprogram", 14 | "libVersion": "2.2.0", 15 | "appid": "", 16 | "projectname": "mp_canvas_drawer", 17 | "isGameTourist": false, 18 | "condition": { 19 | "search": { 20 | "current": -1, 21 | "list": [] 22 | }, 23 | "conversation": { 24 | "current": -1, 25 | "list": [] 26 | }, 27 | "plugin": { 28 | "current": -1, 29 | "list": [] 30 | }, 31 | "game": { 32 | "currentL": -1, 33 | "list": [] 34 | }, 35 | "miniprogram": { 36 | "current": -1, 37 | "list": [ 38 | { 39 | "id": -1, 40 | "name": "多图绘制", 41 | "pathName": "pages/multiple/multiple", 42 | "query": "" 43 | }, 44 | { 45 | "id": -1, 46 | "name": "普通绘制", 47 | "pathName": "pages/index/index", 48 | "query": "" 49 | } 50 | ] 51 | } 52 | } 53 | } --------------------------------------------------------------------------------