├── README.md ├── app.js ├── app.json ├── app.wxss ├── pages ├── index │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss ├── logs │ ├── logs.js │ ├── logs.json │ ├── logs.wxml │ └── logs.wxss └── signature │ ├── signature.js │ ├── signature.json │ ├── signature.wxml │ └── signature.wxss ├── project.config.json ├── resources └── images │ ├── color_black.png │ ├── color_black_selected.png │ ├── color_red.png │ ├── color_red_selected.png │ └── handwriting.gif └── utils └── util.js /README.md: -------------------------------------------------------------------------------- 1 | 2 | wechat-signature 3 | 4 | ### 转载:[查看原版](https://github.com/momtboy/handwriting-weapp) https://github.com/momtboy/handwriting-weapp 5 | 6 | 微信小程序Canvas手写板(use canvas in weapp for user signature) 7 | 8 | 9 | 工作中公司业务需要的微信小程序用户签字功能 准备将其组件化,并加强功能性开发,优化渲染逻辑 10 | 11 | ### 更新计划 12 | 组件化. 13 | 14 | 优化setData过于频繁照成的渲染延迟. 15 | 16 | 增加笔迹样式. 17 | 18 | #### 在源代码中 添加 保存、预览、上传等 -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | //app.js 2 | App({ 3 | onLaunch: function () { 4 | // 展示本地存储能力 5 | var logs = wx.getStorageSync('logs') || [] 6 | logs.unshift(Date.now()) 7 | wx.setStorageSync('logs', logs) 8 | 9 | // 登录 10 | wx.login({ 11 | success: res => { 12 | // 发送 res.code 到后台换取 openId, sessionKey, unionId 13 | } 14 | }) 15 | // 获取用户信息 16 | wx.getSetting({ 17 | success: res => { 18 | if (res.authSetting['scope.userInfo']) { 19 | // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框 20 | wx.getUserInfo({ 21 | success: res => { 22 | // 可以将 res 发送给后台解码出 unionId 23 | this.globalData.userInfo = res.userInfo 24 | 25 | // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 26 | // 所以此处加入 callback 以防止这种情况 27 | if (this.userInfoReadyCallback) { 28 | this.userInfoReadyCallback(res) 29 | } 30 | } 31 | }) 32 | } 33 | } 34 | }) 35 | }, 36 | globalData: { 37 | userInfo: null 38 | } 39 | }) -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages":[ 3 | "pages/signature/signature", 4 | "pages/index/index", 5 | "pages/logs/logs" 6 | ], 7 | "window":{ 8 | "backgroundTextStyle":"light", 9 | "navigationBarBackgroundColor": "#fff", 10 | "navigationBarTitleText": "WeChat", 11 | "navigationBarTextStyle":"black" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app.wxss: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ 2 | .container { 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: space-between; 8 | padding: 200rpx 0; 9 | box-sizing: border-box; 10 | } 11 | -------------------------------------------------------------------------------- /pages/index/index.js: -------------------------------------------------------------------------------- 1 | //index.js 2 | //获取应用实例 3 | const app = getApp() 4 | 5 | Page({ 6 | data: { 7 | motto: 'Hello World', 8 | userInfo: {}, 9 | hasUserInfo: false, 10 | canIUse: wx.canIUse('button.open-type.getUserInfo') 11 | }, 12 | //事件处理函数 13 | bindViewTap: function() { 14 | wx.navigateTo({ 15 | url: '../logs/logs' 16 | }) 17 | }, 18 | onLoad: function () { 19 | if (app.globalData.userInfo) { 20 | this.setData({ 21 | userInfo: app.globalData.userInfo, 22 | hasUserInfo: true 23 | }) 24 | } else if (this.data.canIUse){ 25 | // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 26 | // 所以此处加入 callback 以防止这种情况 27 | app.userInfoReadyCallback = res => { 28 | this.setData({ 29 | userInfo: res.userInfo, 30 | hasUserInfo: true 31 | }) 32 | } 33 | } else { 34 | // 在没有 open-type=getUserInfo 版本的兼容处理 35 | wx.getUserInfo({ 36 | success: res => { 37 | app.globalData.userInfo = res.userInfo 38 | this.setData({ 39 | userInfo: res.userInfo, 40 | hasUserInfo: true 41 | }) 42 | } 43 | }) 44 | } 45 | }, 46 | getUserInfo: function(e) { 47 | console.log(e) 48 | app.globalData.userInfo = e.detail.userInfo 49 | this.setData({ 50 | userInfo: e.detail.userInfo, 51 | hasUserInfo: true 52 | }) 53 | } 54 | }) 55 | -------------------------------------------------------------------------------- /pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {} 3 | } -------------------------------------------------------------------------------- /pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{userInfo.nickName}} 8 | 9 | 10 | 11 | {{motto}} 12 | 13 | 14 | -------------------------------------------------------------------------------- /pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | /**index.wxss**/ 2 | .userinfo { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | .userinfo-avatar { 9 | width: 128rpx; 10 | height: 128rpx; 11 | margin: 20rpx; 12 | border-radius: 50%; 13 | } 14 | 15 | .userinfo-nickname { 16 | color: #aaa; 17 | } 18 | 19 | .usermotto { 20 | margin-top: 200px; 21 | } -------------------------------------------------------------------------------- /pages/logs/logs.js: -------------------------------------------------------------------------------- 1 | //logs.js 2 | const util = require('../../utils/util.js') 3 | 4 | Page({ 5 | data: { 6 | logs: [] 7 | }, 8 | onLoad: function () { 9 | this.setData({ 10 | logs: (wx.getStorageSync('logs') || []).map(log => { 11 | return util.formatTime(new Date(log)) 12 | }) 13 | }) 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /pages/logs/logs.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "查看启动日志", 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /pages/logs/logs.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{index + 1}}. {{log}} 5 | 6 | 7 | -------------------------------------------------------------------------------- /pages/logs/logs.wxss: -------------------------------------------------------------------------------- 1 | .log-list { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 40rpx; 5 | } 6 | .log-item { 7 | margin: 10rpx; 8 | } 9 | -------------------------------------------------------------------------------- /pages/signature/signature.js: -------------------------------------------------------------------------------- 1 | // pages/signature/signature.js 2 | Page({ 3 | 4 | /** 5 | * 页面的初始数据 6 | */ 7 | data: { 8 | canvasName: 'handWriting', 9 | ctx: '', 10 | canvasWidth: 0, 11 | canvasHeight: 0, 12 | transparent: 1, // 透明度 13 | selectColor: 'black', 14 | lineColor: '#1A1A1A', // 颜色 15 | lineSize: 1.5, // 笔记倍数 16 | lineMin: 0.5, // 最小笔画半径 17 | lineMax: 4, // 最大笔画半径 18 | pressure: 1, // 默认压力 19 | smoothness: 60, //顺滑度,用60的距离来计算速度 20 | currentPoint: {}, 21 | currentLine: [], // 当前线条 22 | firstTouch: true, // 第一次触发 23 | radius: 1, //画圆的半径 24 | cutArea: { top: 0, right: 0, bottom: 0, left: 0 }, //裁剪区域 25 | bethelPoint: [], //保存所有线条 生成的贝塞尔点; 26 | lastPoint: 0, 27 | chirography: [], //笔迹 28 | currentChirography: {}, //当前笔迹 29 | linePrack: [] //划线轨迹 , 生成线条的实际点 30 | }, 31 | 32 | /*======所有自定义函数======*/ 33 | 34 | // 笔迹开始 35 | uploadScaleStart(e) { 36 | if (e.type != 'touchstart') return false; 37 | let ctx = this.data.ctx; 38 | ctx.setFillStyle(this.data.lineColor); // 初始线条设置颜色 39 | ctx.setGlobalAlpha(this.data.transparent); // 设置半透明 40 | let currentPoint = { 41 | x: e.touches[0].x, 42 | y: e.touches[0].y 43 | } 44 | let currentLine = this.data.currentLine; 45 | currentLine.unshift({ 46 | time: new Date().getTime(), 47 | dis: 0, 48 | x: currentPoint.x, 49 | y: currentPoint.y 50 | }) 51 | this.setData({ 52 | currentPoint, 53 | // currentLine 54 | }) 55 | if (this.data.firstTouch) { 56 | this.setData({ 57 | cutArea: { top: currentPoint.y, right: currentPoint.x, bottom: currentPoint.y, left: currentPoint.x }, 58 | firstTouch: false 59 | }) 60 | } 61 | this.pointToLine(currentLine); 62 | }, 63 | // 笔迹移动 64 | uploadScaleMove(e) { 65 | if (e.type != 'touchmove') return false; 66 | if (e.cancelable) { 67 | // 判断默认行为是否已经被禁用 68 | if (!e.defaultPrevented) { 69 | e.preventDefault(); 70 | } 71 | } 72 | let point = { 73 | x: e.touches[0].x, 74 | y: e.touches[0].y 75 | } 76 | 77 | //测试裁剪 78 | if (point.y < this.data.cutArea.top) { 79 | this.data.cutArea.top = point.y; 80 | } 81 | if (point.y < 0) this.data.cutArea.top = 0; 82 | 83 | if (point.x > this.data.cutArea.right) { 84 | this.data.cutArea.right = point.x; 85 | } 86 | if (this.data.canvasWidth - point.x <= 0) { 87 | this.data.cutArea.right = this.data.canvasWidth; 88 | } 89 | if (point.y > this.data.cutArea.bottom) { 90 | this.data.cutArea.bottom = point.y; 91 | } 92 | if (this.data.canvasHeight - point.y <= 0) { 93 | this.data.cutArea.bottom = this.data.canvasHeight; 94 | } 95 | if (point.x < this.data.cutArea.left) { 96 | this.data.cutArea.left = point.x; 97 | } 98 | if (point.x < 0) this.data.cutArea.left = 0; 99 | 100 | this.setData({ 101 | lastPoint: this.data.currentPoint, 102 | currentPoint: point 103 | }) 104 | let currentLine = this.data.currentLine 105 | currentLine.unshift({ 106 | time: new Date().getTime(), 107 | dis: this.distance(this.data.currentPoint, this.data.lastPoint), 108 | x: point.x, 109 | y: point.y 110 | }) 111 | // this.setData({ 112 | // currentLine 113 | // }) 114 | this.pointToLine(currentLine); 115 | }, 116 | // 笔迹结束 117 | uploadScaleEnd(e) { 118 | if (e.type != 'touchend') return 0; 119 | let point = { 120 | x: e.changedTouches[0].x, 121 | y: e.changedTouches[0].y 122 | } 123 | this.setData({ 124 | lastPoint: this.data.currentPoint, 125 | currentPoint: point 126 | }) 127 | let currentLine = this.data.currentLine 128 | currentLine.unshift({ 129 | time: new Date().getTime(), 130 | dis: this.distance(this.data.currentPoint, this.data.lastPoint), 131 | x: point.x, 132 | y: point.y 133 | }) 134 | // this.setData({ 135 | // currentLine 136 | // }) 137 | if (currentLine.length > 2) { 138 | var info = (currentLine[0].time - currentLine[currentLine.length - 1].time) / currentLine.length; 139 | //$("#info").text(info.toFixed(2)); 140 | } 141 | //一笔结束,保存笔迹的坐标点,清空,当前笔迹 142 | //增加判断是否在手写区域; 143 | this.pointToLine(currentLine); 144 | var currentChirography = { 145 | lineSize: this.data.lineSize, 146 | lineColor: this.data.lineColor 147 | }; 148 | var chirography = this.data.chirography 149 | chirography.unshift(currentChirography); 150 | this.setData({ 151 | chirography 152 | }) 153 | var linePrack = this.data.linePrack 154 | linePrack.unshift(this.data.currentLine); 155 | this.setData({ 156 | linePrack, 157 | currentLine: [] 158 | }) 159 | }, 160 | 161 | retDraw() { 162 | this.data.ctx.clearRect(0, 0, 700, 730) 163 | this.data.ctx.draw(); 164 | 165 | //设置canvas背景 166 | this.setCanvasBg("#fff"); 167 | }, 168 | 169 | //画两点之间的线条;参数为:line,会绘制最近的开始的两个点; 170 | pointToLine(line) { 171 | this.calcBethelLine(line); 172 | return; 173 | }, 174 | //计算插值的方式; 175 | calcBethelLine(line) { 176 | if (line.length <= 1) { 177 | line[0].r = this.data.radius; 178 | return; 179 | } 180 | let x0, x1, x2, y0, y1, y2, r0, r1, r2, len, lastRadius, dis = 0, time = 0, curveValue = 0.5; 181 | if (line.length <= 2) { 182 | x0 = line[1].x 183 | y0 = line[1].y 184 | x2 = line[1].x + (line[0].x - line[1].x) * curveValue; 185 | y2 = line[1].y + (line[0].y - line[1].y) * curveValue; 186 | //x2 = line[1].x; 187 | //y2 = line[1].y; 188 | x1 = x0 + (x2 - x0) * curveValue; 189 | y1 = y0 + (y2 - y0) * curveValue;; 190 | 191 | } else { 192 | x0 = line[2].x + (line[1].x - line[2].x) * curveValue; 193 | y0 = line[2].y + (line[1].y - line[2].y) * curveValue; 194 | x1 = line[1].x; 195 | y1 = line[1].y; 196 | x2 = x1 + (line[0].x - x1) * curveValue; 197 | y2 = y1 + (line[0].y - y1) * curveValue; 198 | } 199 | //从计算公式看,三个点分别是(x0,y0),(x1,y1),(x2,y2) ;(x1,y1)这个是控制点,控制点不会落在曲线上;实际上,这个点还会手写获取的实际点,却落在曲线上 200 | len = this.distance({ x: x2, y: y2 }, { x: x0, y: y0 }); 201 | lastRadius = this.data.radius; 202 | for (let n = 0; n < line.length - 1; n++) { 203 | dis += line[n].dis; 204 | time += line[n].time - line[n + 1].time; 205 | if (dis > this.data.smoothness) break; 206 | } 207 | this.setData({ 208 | radius: Math.min(time / len * this.data.pressure + this.data.lineMin, this.data.lineMax) * this.data.lineSize 209 | }); 210 | line[0].r = this.data.radius; 211 | //计算笔迹半径; 212 | if (line.length <= 2) { 213 | r0 = (lastRadius + this.data.radius) / 2; 214 | r1 = r0; 215 | r2 = r1; 216 | //return; 217 | } else { 218 | r0 = (line[2].r + line[1].r) / 2; 219 | r1 = line[1].r; 220 | r2 = (line[1].r + line[0].r) / 2; 221 | } 222 | let n = 5; 223 | let point = []; 224 | for (let i = 0; i < n; i++) { 225 | let t = i / (n - 1); 226 | let x = (1 - t) * (1 - t) * x0 + 2 * t * (1 - t) * x1 + t * t * x2; 227 | let y = (1 - t) * (1 - t) * y0 + 2 * t * (1 - t) * y1 + t * t * y2; 228 | let r = lastRadius + (this.data.radius - lastRadius) / n * i; 229 | point.push({ x: x, y: y, r: r }); 230 | if (point.length == 3) { 231 | let a = this.ctaCalc(point[0].x, point[0].y, point[0].r, point[1].x, point[1].y, point[1].r, point[2].x, point[2].y, point[2].r); 232 | a[0].color = this.data.lineColor; 233 | // let bethelPoint = this.data.bethelPoint; 234 | // console.log(a) 235 | // console.log(this.data.bethelPoint) 236 | // bethelPoint = bethelPoint.push(a); 237 | this.bethelDraw(a, 1); 238 | point = [{ x: x, y: y, r: r }]; 239 | } 240 | } 241 | this.setData({ 242 | currentLine: line 243 | }) 244 | }, 245 | //求两点之间距离 246 | distance(a, b) { 247 | let x = b.x - a.x; 248 | let y = b.y - a.y; 249 | return Math.sqrt(x * x + y * y); 250 | }, 251 | ctaCalc(x0, y0, r0, x1, y1, r1, x2, y2, r2) { 252 | let a = [], vx01, vy01, norm, n_x0, n_y0, vx21, vy21, n_x2, n_y2; 253 | vx01 = x1 - x0; 254 | vy01 = y1 - y0; 255 | norm = Math.sqrt(vx01 * vx01 + vy01 * vy01 + 0.0001) * 2; 256 | vx01 = vx01 / norm * r0; 257 | vy01 = vy01 / norm * r0; 258 | n_x0 = vy01; 259 | n_y0 = -vx01; 260 | vx21 = x1 - x2; 261 | vy21 = y1 - y2; 262 | norm = Math.sqrt(vx21 * vx21 + vy21 * vy21 + 0.0001) * 2; 263 | vx21 = vx21 / norm * r2; 264 | vy21 = vy21 / norm * r2; 265 | n_x2 = -vy21; 266 | n_y2 = vx21; 267 | a.push({ mx: x0 + n_x0, my: y0 + n_y0, color: "#1A1A1A" }); 268 | a.push({ c1x: x1 + n_x0, c1y: y1 + n_y0, c2x: x1 + n_x2, c2y: y1 + n_y2, ex: x2 + n_x2, ey: y2 + n_y2 }); 269 | a.push({ c1x: x2 + n_x2 - vx21, c1y: y2 + n_y2 - vy21, c2x: x2 - n_x2 - vx21, c2y: y2 - n_y2 - vy21, ex: x2 - n_x2, ey: y2 - n_y2 }); 270 | a.push({ c1x: x1 - n_x2, c1y: y1 - n_y2, c2x: x1 - n_x0, c2y: y1 - n_y0, ex: x0 - n_x0, ey: y0 - n_y0 }); 271 | a.push({ c1x: x0 - n_x0 - vx01, c1y: y0 - n_y0 - vy01, c2x: x0 + n_x0 - vx01, c2y: y0 + n_y0 - vy01, ex: x0 + n_x0, ey: y0 + n_y0 }); 272 | a[0].mx = a[0].mx.toFixed(1); 273 | a[0].mx = parseFloat(a[0].mx); 274 | a[0].my = a[0].my.toFixed(1); 275 | a[0].my = parseFloat(a[0].my); 276 | for (let i = 1; i < a.length; i++) { 277 | a[i].c1x = a[i].c1x.toFixed(1); 278 | a[i].c1x = parseFloat(a[i].c1x); 279 | a[i].c1y = a[i].c1y.toFixed(1); 280 | a[i].c1y = parseFloat(a[i].c1y); 281 | a[i].c2x = a[i].c2x.toFixed(1); 282 | a[i].c2x = parseFloat(a[i].c2x); 283 | a[i].c2y = a[i].c2y.toFixed(1); 284 | a[i].c2y = parseFloat(a[i].c2y); 285 | a[i].ex = a[i].ex.toFixed(1); 286 | a[i].ex = parseFloat(a[i].ex); 287 | a[i].ey = a[i].ey.toFixed(1); 288 | a[i].ey = parseFloat(a[i].ey); 289 | } 290 | return a; 291 | }, 292 | bethelDraw(point, is_fill, color) { 293 | let ctx = this.data.ctx; 294 | ctx.beginPath(); 295 | ctx.moveTo(point[0].mx, point[0].my); 296 | if (undefined != color) { 297 | ctx.setFillStyle(color); 298 | ctx.setStrokeStyle(color); 299 | } else { 300 | ctx.setFillStyle(point[0].color); 301 | ctx.setStrokeStyle(point[0].color); 302 | } 303 | for (let i = 1; i < point.length; i++) { 304 | ctx.bezierCurveTo(point[i].c1x, point[i].c1y, point[i].c2x, point[i].c2y, point[i].ex, point[i].ey); 305 | } 306 | ctx.stroke(); 307 | if (undefined != is_fill) { 308 | ctx.fill(); //填充图形 ( 后绘制的图形会覆盖前面的图形, 绘制时注意先后顺序 ) 309 | } 310 | ctx.draw(true) 311 | }, 312 | selectColorEvent(event) { 313 | console.log(event) 314 | var color = event.currentTarget.dataset.colorValue; 315 | var colorSelected = event.currentTarget.dataset.color; 316 | this.setData({ 317 | selectColor: colorSelected, 318 | lineColor: color 319 | }) 320 | }, 321 | 322 | //将Canvas内容转成 临时图片 --> cb 为回调函数 形参 tempImgPath 为 生成的图片临时路径 323 | canvasToImg(cb){ //这种写法移动端 出不来 324 | 325 | this.data.ctx.draw(true, () => { 326 | wx.canvasToTempFilePath({ 327 | canvasId: 'handWriting', 328 | fileType: 'png', 329 | quality: 1, //图片质量 330 | success(res) { 331 | // console.log(res.tempFilePath, 'canvas生成图片地址'); 332 | 333 | wx.showToast({ 334 | title: '执行了吗?', 335 | }) 336 | 337 | cb(res.tempFilePath); 338 | } 339 | 340 | }) 341 | }); 342 | 343 | 344 | }, 345 | 346 | 347 | //完成 348 | subCanvas(){ 349 | // console.log(121); 350 | 351 | 352 | /* 353 | this.data.ctx.draw( true, ()=>{ 354 | wx.canvasToTempFilePath({ 355 | canvasId: 'handWriting', 356 | fileType: 'png', 357 | quality: 1, //图片质量 358 | success(res){ 359 | */ 360 | 361 | 362 | // console.log(res.tempFilePath, 'canvas生成图片地址'); 363 | /* 364 | wx.showModal({ 365 | title: '哈哈啊', 366 | content: '这是什么', 367 | }) 368 | */ 369 | /* 370 | wx.showToast({ 371 | title: '以保存', 372 | }) 373 | */ 374 | 375 | 376 | 377 | //保存到系统相册 378 | /* 379 | wx.saveImageToPhotosAlbum({ 380 | filePath: res.tempFilePath, 381 | success(res) { 382 | 383 | console.log(res,'保存res'); 384 | 385 | wx.showToast( { 386 | title: '已成功保存到相册', 387 | duration: 2000 388 | } ); 389 | 390 | } 391 | }) 392 | */ 393 | 394 | 395 | 396 | /* 397 | 398 | } 399 | }) 400 | } ); 401 | */ 402 | 403 | 404 | 405 | 406 | 407 | }, 408 | 409 | //保存到相册 410 | saveCanvasAsImg(){ 411 | console.log(1212); 412 | 413 | /* 414 | this.canvasToImg( tempImgPath=>{ 415 | // console.log(tempImgPath, '临时路径'); 416 | 417 | wx.saveImageToPhotosAlbum({ 418 | filePath: tempImgPath, 419 | success(res) { 420 | 421 | wx.showToast({ 422 | title: '已保存到相册', 423 | duration: 2000 424 | }); 425 | 426 | } 427 | }) 428 | 429 | } ); 430 | */ 431 | 432 | wx.canvasToTempFilePath({ 433 | canvasId: 'handWriting', 434 | fileType: 'png', 435 | quality: 1, //图片质量 436 | success(res) { 437 | // console.log(res.tempFilePath, 'canvas生成图片地址'); 438 | wx.saveImageToPhotosAlbum({ 439 | filePath: res.tempFilePath, 440 | success(res) { 441 | 442 | wx.showToast({ 443 | title: '已保存到相册', 444 | duration: 2000 445 | }); 446 | 447 | } 448 | }) 449 | 450 | 451 | } 452 | 453 | }) 454 | 455 | 456 | 457 | }, 458 | 459 | //预览 460 | previewCanvasImg(){ 461 | 462 | 463 | 464 | wx.canvasToTempFilePath({ 465 | canvasId: 'handWriting', 466 | fileType: 'jpg', 467 | quality: 1, //图片质量 468 | success(res) { 469 | // console.log(res.tempFilePath, 'canvas生成图片地址'); 470 | 471 | 472 | wx.previewImage({ 473 | urls: [res.tempFilePath], //预览图片 数组 474 | }) 475 | 476 | } 477 | 478 | }) 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | /* //移动端出不来 ^~^!! 491 | 492 | this.canvasToImg( tempImgPath=>{ 493 | 494 | wx.previewImage({ 495 | urls: [tempImgPath], //预览图片 数组 496 | }) 497 | 498 | 499 | } ); 500 | 501 | */ 502 | 503 | 504 | 505 | }, 506 | 507 | //上传 508 | uploadCanvasImg() { 509 | // console.log(999); 510 | 511 | wx.canvasToTempFilePath({ 512 | canvasId: 'handWriting', 513 | fileType: 'png', 514 | quality: 1, //图片质量 515 | success(res) { 516 | // console.log(res.tempFilePath, 'canvas生成图片地址'); 517 | 518 | //上传 519 | wx.uploadFile({ 520 | url: 'https://example.weixin.qq.com/upload', // 仅为示例,非真实的接口地址 521 | filePath: res.tempFilePath, 522 | name: 'file_signature', 523 | formData: { 524 | user: 'test' 525 | }, 526 | success(res) { 527 | const data = res.data 528 | // do something 529 | } 530 | }) 531 | 532 | } 533 | 534 | }) 535 | 536 | 537 | }, 538 | 539 | //设置canvas背景色 不设置 导出的canvas的背景为透明 540 | //@params:字符串 color 541 | setCanvasBg(color){ 542 | 543 | console.log(999); 544 | /* 将canvas背景设置为 白底,不设置 导出的canvas的背景为透明 */ 545 | //rect() 参数说明 矩形路径左上角的横坐标,左上角的纵坐标, 矩形路径的宽度, 矩形路径的高度 546 | //这里是 canvasHeight - 4 是因为下边盖住边框了,所以手动减了写 547 | this.data.ctx.rect(0, 0, this.data.canvasWidth, this.data.canvasHeight - 4); 548 | // ctx.setFillStyle('red') 549 | this.data.ctx.setFillStyle( color ) 550 | this.data.ctx.fill() //设置填充 551 | this.data.ctx.draw() //开画 552 | 553 | 554 | }, 555 | 556 | 557 | 558 | 559 | 560 | 561 | /*======所有自定义函数=END=====*/ 562 | 563 | 564 | 565 | 566 | 567 | /** 568 | * 生命周期函数--监听页面加载 569 | */ 570 | onLoad: function (options) { 571 | 572 | let canvasName = this.data.canvasName 573 | let ctx = wx.createCanvasContext(canvasName) 574 | this.setData({ 575 | ctx: ctx 576 | }) 577 | var query = wx.createSelectorQuery(); 578 | query.select('.handCenter').boundingClientRect( rect => { 579 | 580 | this.setData({ 581 | canvasWidth: rect.width, 582 | canvasHeight: rect.height 583 | }) 584 | 585 | /* 将canvas背景设置为 白底,不设置 导出的canvas的背景为透明 */ 586 | // console.log(this, 'hahah'); 587 | this.setCanvasBg('#fff'); 588 | 589 | 590 | }).exec(); 591 | 592 | }, 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | /** 601 | * 生命周期函数--监听页面初次渲染完成 602 | */ 603 | onReady: function () { 604 | 605 | }, 606 | 607 | /** 608 | * 生命周期函数--监听页面显示 609 | */ 610 | onShow: function () { 611 | 612 | }, 613 | 614 | /** 615 | * 生命周期函数--监听页面隐藏 616 | */ 617 | onHide: function () { 618 | 619 | }, 620 | 621 | /** 622 | * 生命周期函数--监听页面卸载 623 | */ 624 | onUnload: function () { 625 | 626 | }, 627 | 628 | /** 629 | * 页面相关事件处理函数--监听用户下拉动作 630 | */ 631 | onPullDownRefresh: function () { 632 | 633 | }, 634 | 635 | /** 636 | * 页面上拉触底事件的处理函数 637 | */ 638 | onReachBottom: function () { 639 | 640 | }, 641 | 642 | /** 643 | * 用户点击右上角分享 644 | */ 645 | onShareAppMessage: function () { 646 | 647 | } 648 | }) -------------------------------------------------------------------------------- /pages/signature/signature.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {} 3 | } -------------------------------------------------------------------------------- /pages/signature/signature.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 手写板 25 | 26 | -------------------------------------------------------------------------------- /pages/signature/signature.wxss: -------------------------------------------------------------------------------- 1 | /* pages/signature/signature.wxss */ 2 | page { 3 | background: #fbfbfb; 4 | height: auto; 5 | overflow: hidden; 6 | } 7 | 8 | .wrapper { 9 | width: 100%; 10 | height: 95vh; 11 | margin: 30rpx 0; 12 | overflow: hidden; 13 | display: flex; 14 | align-content: center; 15 | flex-direction: row; 16 | justify-content: center; 17 | font-size: 28rpx; 18 | } 19 | 20 | .handWriting { 21 | background: #fff; 22 | width: 100%; 23 | height: 95vh; 24 | } 25 | 26 | .handRight { 27 | display: inline-flex; 28 | align-items: center; 29 | } 30 | 31 | .handCenter { 32 | border: 4rpx dashed #e9e9e9; 33 | flex: 5; 34 | overflow: hidden; 35 | box-sizing: border-box; 36 | } 37 | 38 | .handTitle { 39 | transform: rotate(90deg); 40 | flex: 1; 41 | color: #666; 42 | } 43 | 44 | .handBtn button { 45 | font-size: 28rpx; 46 | } 47 | 48 | .handBtn { 49 | height: 95vh; 50 | display: inline-flex; 51 | flex-direction: column; 52 | justify-content: space-between; 53 | align-content: space-between; 54 | flex: 1; 55 | } 56 | 57 | .delBtn { 58 | position: absolute; 59 | top: 250rpx; 60 | left: 0rpx; 61 | transform: rotate(90deg); 62 | color: #666; 63 | } 64 | 65 | .delBtn image { 66 | position: absolute; 67 | top: 13rpx; 68 | left: 25rpx; 69 | } 70 | 71 | .subBtn { 72 | position: absolute; 73 | bottom: 52rpx; 74 | left: -3rpx; 75 | display: inline-flex; 76 | transform: rotate(90deg); 77 | background: #008ef6; 78 | color: #fff; 79 | margin-bottom: 30rpx; 80 | text-align: center; 81 | justify-content: center; 82 | } 83 | 84 | /*Peach - 新增 - 保存*/ 85 | 86 | .saveBtn { 87 | position: absolute; 88 | top: 375rpx; 89 | left: 0rpx; 90 | transform: rotate(90deg); 91 | color: #666; 92 | } 93 | 94 | 95 | .previewBtn { 96 | position: absolute; 97 | top: 500rpx; 98 | left: 0rpx; 99 | transform: rotate(90deg); 100 | color: #666; 101 | } 102 | 103 | .uploadBtn { 104 | position: absolute; 105 | top: 625rpx; 106 | left: 0rpx; 107 | transform: rotate(90deg); 108 | color: #666; 109 | } 110 | 111 | 112 | /*Peach - 新增 - 保存*/ 113 | 114 | 115 | 116 | 117 | 118 | .black-select { 119 | width: 60rpx; 120 | height: 60rpx; 121 | position: absolute; 122 | top: 30rpx; 123 | left: 25rpx; 124 | } 125 | .black-select.color_select { 126 | width: 90rpx; 127 | height: 90rpx; 128 | top: 30rpx; 129 | left: 10rpx; 130 | } 131 | .red-select { 132 | width: 60rpx; 133 | height: 60rpx; 134 | position: absolute; 135 | top:140rpx; 136 | left:25rpx; 137 | } 138 | .red-select.color_select { 139 | width: 90rpx; 140 | height: 90rpx; 141 | top: 120rpx; 142 | left: 10rpx; 143 | } -------------------------------------------------------------------------------- /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 | "autoAudits": false 13 | }, 14 | "compileType": "miniprogram", 15 | "libVersion": "2.6.6", 16 | "appid": "wxe8d475f455cac188", 17 | "projectname": "wechat-signature", 18 | "debugOptions": { 19 | "hidedInDevtools": [] 20 | }, 21 | "isGameTourist": false, 22 | "condition": { 23 | "search": { 24 | "current": -1, 25 | "list": [] 26 | }, 27 | "conversation": { 28 | "current": -1, 29 | "list": [] 30 | }, 31 | "game": { 32 | "currentL": -1, 33 | "list": [] 34 | }, 35 | "miniprogram": { 36 | "current": -1, 37 | "list": [] 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /resources/images/color_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kayakyx/wechat-signature/74c6aa0c8e3e57f9d3e0d196b30e7cedec747b5a/resources/images/color_black.png -------------------------------------------------------------------------------- /resources/images/color_black_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kayakyx/wechat-signature/74c6aa0c8e3e57f9d3e0d196b30e7cedec747b5a/resources/images/color_black_selected.png -------------------------------------------------------------------------------- /resources/images/color_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kayakyx/wechat-signature/74c6aa0c8e3e57f9d3e0d196b30e7cedec747b5a/resources/images/color_red.png -------------------------------------------------------------------------------- /resources/images/color_red_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kayakyx/wechat-signature/74c6aa0c8e3e57f9d3e0d196b30e7cedec747b5a/resources/images/color_red_selected.png -------------------------------------------------------------------------------- /resources/images/handwriting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kayakyx/wechat-signature/74c6aa0c8e3e57f9d3e0d196b30e7cedec747b5a/resources/images/handwriting.gif -------------------------------------------------------------------------------- /utils/util.js: -------------------------------------------------------------------------------- 1 | const formatTime = date => { 2 | const year = date.getFullYear() 3 | const month = date.getMonth() + 1 4 | const day = date.getDate() 5 | const hour = date.getHours() 6 | const minute = date.getMinutes() 7 | const second = date.getSeconds() 8 | 9 | return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':') 10 | } 11 | 12 | const formatNumber = n => { 13 | n = n.toString() 14 | return n[1] ? n : '0' + n 15 | } 16 | 17 | module.exports = { 18 | formatTime: formatTime 19 | } 20 | --------------------------------------------------------------------------------