├── 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 |
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 |
--------------------------------------------------------------------------------