###Foreword
Recently, the nail applet developed by the company requires handwritten signature. I wrote one. Let's see the effect first. Wechat applet can also be used.
####The first step is page layout
I found an example of handwritten signature on the Internet, copied and modified it, and posted the code directly:
//Layout code <view class="wrapper"> //Signature display area <view class="handCenter"> <view class="ats"> //Signature drawing canvas (the first canvas will be described later) <canvas class="handWriting" disable-scroll="true" onTouchStart="uploadScaleStart" onTouchMove="uploadScaleMove" onTouchEnd="uploadScaleEnd" onTap="mouseDown" id="handWriting"> </canvas> //Signature rotation canvas (the second canvas will be described later) <canvas class="handWriting2" id="canvas"></canvas> </view> //Operation button <view class="handBtn"> <button onTap="subCanvas" class="subBtn">complete</button> <button onTap="retDraw" class="delBtn">Reset</button> </view> </view> //title <view class="handRight"> <text class="handTitle">autograph</text> </view> </view>
####The second step is css style (only the important ones are pasted completely at the bottom)
Because the applet does not support screen rotation (it does not seem to support it at present), the layout control will rotate 90 °, which is the following property
transform: rotate(90deg);
The second canvas style should be hidden under the first one and not displayed. It is only used to generate rotating pictures:
.ats{ position: relative; } //First canvas .handWriting { position: relative; background: #fff; width: 98%; z-index: 99; height: 83vh; border: 4rpx dashed #e9e9e9; } //Second canvas .handWriting2{ position: absolute; z-index: 9; width: 600rpx; height: 300rpx; top: 0; left: 0; /* transform: rotate(90deg); */ background-color: #FFF; }
####Step 3: JS code (only say the important ones and paste them completely at the bottom)
I won't say how to draw the handwriting. I also copied it. I said the problems I encountered and solved.
When we finish writing, we need to save. We all know that we need to use the method of toTempFilePath (converting Canvas content into temporary pictures, which is different from wechat applet methods), but the saved pictures are vertical, like this:
What we need to do is to rotate. We can't rotate the first canvas. The experience is too bad, so the second hidden canvas is useful. Method:
drowImage(path) { //This path is the temporary path of the first canvas. Bring it here var canvasContext = my.createCanvasContext('canvas')//Unique identification of the second canvas canvasContext.scale(0.5, 0.5);//Double the size of the first canvas, because my first one is relatively large and can be adjusted by myself. It is too large and the display is incomplete canvasContext.translate(0, 300)//This is the origin offset (the height of the second canvas is 300 and the width is 600) canvasContext.rotate(-90 * Math.PI / 180)//Rotate 90 ° counterclockwise to get the horizontal signature canvasContext.drawImage(path, 0, 0, 300, 600)//Draw the first one on the second canvasContext.draw(true)//Note that it takes time to draw this place. You need to delay obtaining the temporary picture below, otherwise you may get blank because the draw() method is not completed setTimeout(() => { //Convert the second Canvas content to a temporary picture canvasContext.toTempFilePath({ success(res) { console.log("AAAAAFFFFF" + res.filePath) //Upload pictures dd.uploadFile({ url: 'https://www.aa./ API / qiniupload / upload ', / / your address filePath: res.filePath, fileType: 'image', fileName: 'file', header: { 'content-type': 'multipart/form-data' }, success: (res) => { // res.data is the relevant data returned by your backend const data = JSON.parse(res.data) var urls = data.data var pages = getCurrentPages(); // Get page stack var prevPage = pages[pages.length - 2]; // Previous page console.log("AAAAAFFFFF" + urls) md5.gone(); prevPage.setData({ qian: urls // Send the picture address back to the previous page }) my.navigateBack({//return delta: 1 }) }, fial: (res) => { console.log("AAAAAFFFFF" + res) } }) } }) }, 1000) },
Then I got this picture:
####Step 4: summary
One of the most troublesome problems is to rotate the picture. I don't know if there is any other better method (maybe I'm too delicious). I can solve this method. The second problem is that when drawing () redraws, I always get blank!!!!, Finally, I found that I need to delay the setTimeout before taking it. As mentioned above, finally paste all the codes and copy them directly. The address of the uploaded picture needs to be changed.
####JS all codes
Page({ /** * Initial data of the page */ data: { canvasName: 'handWriting', ctx: '', canvasWidth: 0, canvasHeight: 0, transparent: 1, // transparency selectColor: 'black', lineColor: '#1a1a ', / / color lineSize: 1.5, // Note multiple lineMin: 0.5, // Minimum stroke radius lineMax: 4, // Maximum stroke radius pressure: 1, // Default pressure smoothness: 60, //Smoothness, the speed is calculated with a distance of 60 currentPoint: {}, currentLine: [], // Current line firstTouch: true, // First trigger radius: 1, //Radius of circle cutArea: { top: 0, right: 0, bottom: 0, left: 0 }, //Clipping Region bethelPoint: [], //Save Bezier points generated by all lines; lastPoint: 0, chirography: [], //Handwriting currentChirography: {}, //Current handwriting linePrack: [] //Scribe track to generate the actual points of the line }, /*======All custom functions======*/ // Handwriting start uploadScaleStart(e) { if (e.type != 'touchStart') return false; let ctx = this.data.ctx; ctx.setFillStyle(this.data.lineColor); // Initial line set color ctx.setGlobalAlpha(this.data.transparent); // Set translucency let currentPoint = { x: e.touches[0].x, y: e.touches[0].y } let currentLine = this.data.currentLine; currentLine.unshift({ time: new Date().getTime(), dis: 0, x: currentPoint.x, y: currentPoint.y }) this.setData({ currentPoint, // currentLine }) if (this.data.firstTouch) { this.setData({ cutArea: { top: currentPoint.y, right: currentPoint.x, bottom: currentPoint.y, left: currentPoint.x }, firstTouch: false }) } this.pointToLine(currentLine); }, // Handwriting movement uploadScaleMove(e) { if (e.type != 'touchMove') return false; if (e.cancelable) { // Determines whether the default behavior has been disabled if (!e.defaultPrevented) { e.preventDefault(); } } let point = { x: e.touches[0].x, y: e.touches[0].y } //Test clipping if (point.y < this.data.cutArea.top) { this.data.cutArea.top = point.y; } if (point.y < 0) this.data.cutArea.top = 0; if (point.x > this.data.cutArea.right) { this.data.cutArea.right = point.x; } if (this.data.canvasWidth - point.x <= 0) { this.data.cutArea.right = this.data.canvasWidth; } if (point.y > this.data.cutArea.bottom) { this.data.cutArea.bottom = point.y; } if (this.data.canvasHeight - point.y <= 0) { this.data.cutArea.bottom = this.data.canvasHeight; } if (point.x < this.data.cutArea.left) { this.data.cutArea.left = point.x; } if (point.x < 0) this.data.cutArea.left = 0; this.setData({ lastPoint: this.data.currentPoint, currentPoint: point }) let currentLine = this.data.currentLine currentLine.unshift({ time: new Date().getTime(), dis: this.distance(this.data.currentPoint, this.data.lastPoint), x: point.x, y: point.y }) // this.setData({ // currentLine // }) this.pointToLine(currentLine); }, // End of handwriting uploadScaleEnd(e) { if (e.type != 'touchEnd') return 0; let point = { x: e.changedTouches[0].x, y: e.changedTouches[0].y } this.setData({ lastPoint: this.data.currentPoint, currentPoint: point }) let currentLine = this.data.currentLine currentLine.unshift({ time: new Date().getTime(), dis: this.distance(this.data.currentPoint, this.data.lastPoint), x: point.x, y: point.y }) // this.setData({ // currentLine // }) if (currentLine.length > 2) { var info = (currentLine[0].time - currentLine[currentLine.length - 1].time) / currentLine.length; //$("#info").text(info.toFixed(2)); } //At the end of a stroke, save the coordinate points of the handwriting, clear the current handwriting //Add judgment whether it is in the handwriting area; this.pointToLine(currentLine); var currentChirography = { lineSize: this.data.lineSize, lineColor: this.data.lineColor }; var chirography = this.data.chirography chirography.unshift(currentChirography); this.setData({ chirography }) var linePrack = this.data.linePrack linePrack.unshift(this.data.currentLine); this.setData({ linePrack, currentLine: [] }) }, retDraw() { this.data.ctx.clearRect(0, 0, 700, 730) this.data.ctx.draw(); //Set canvas background this.setCanvasBg("#fff"); }, //Draw a line between two points; The parameter is: line, and the two nearest starting points will be drawn; pointToLine(line) { this.calcBethelLine(line); return; }, //How to calculate interpolation; calcBethelLine(line) { if (line.length <= 1) { line[0].r = this.data.radius; return; } let x0, x1, x2, y0, y1, y2, r0, r1, r2, len, lastRadius, dis = 0, time = 0, curveValue = 0.5; if (line.length <= 2) { x0 = line[1].x y0 = line[1].y x2 = line[1].x + (line[0].x - line[1].x) * curveValue; y2 = line[1].y + (line[0].y - line[1].y) * curveValue; //x2 = line[1].x; //y2 = line[1].y; x1 = x0 + (x2 - x0) * curveValue; y1 = y0 + (y2 - y0) * curveValue;; } else { x0 = line[2].x + (line[1].x - line[2].x) * curveValue; y0 = line[2].y + (line[1].y - line[2].y) * curveValue; x1 = line[1].x; y1 = line[1].y; x2 = x1 + (line[0].x - x1) * curveValue; y2 = y1 + (line[0].y - y1) * curveValue; } //From the calculation formula, the three points are (x0,y0),(x1,y1),(x2,y2); (x1,y1) this is the control point, which will not fall on the curve; In fact, this point will also be the actual point obtained by handwriting, but it falls on the curve len = this.distance({ x: x2, y: y2 }, { x: x0, y: y0 }); lastRadius = this.data.radius; for (let n = 0; n < line.length - 1; n++) { dis += line[n].dis; time += line[n].time - line[n + 1].time; if (dis > this.data.smoothness) break; } this.setData({ radius: Math.min(time / len * this.data.pressure + this.data.lineMin, this.data.lineMax) * this.data.lineSize }); line[0].r = this.data.radius; //Calculate the handwriting radius; if (line.length <= 2) { r0 = (lastRadius + this.data.radius) / 2; r1 = r0; r2 = r1; //return; } else { r0 = (line[2].r + line[1].r) / 2; r1 = line[1].r; r2 = (line[1].r + line[0].r) / 2; } let n = 5; let point = []; for (let i = 0; i < n; i++) { let t = i / (n - 1); let x = (1 - t) * (1 - t) * x0 + 2 * t * (1 - t) * x1 + t * t * x2; let y = (1 - t) * (1 - t) * y0 + 2 * t * (1 - t) * y1 + t * t * y2; let r = lastRadius + (this.data.radius - lastRadius) / n * i; point.push({ x: x, y: y, r: r }); if (point.length == 3) { 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); a[0].color = this.data.lineColor; // let bethelPoint = this.data.bethelPoint; // console.log(a) // console.log(this.data.bethelPoint) // bethelPoint = bethelPoint.push(a); this.bethelDraw(a, 1); point = [{ x: x, y: y, r: r }]; } } this.setData({ currentLine: line }) }, //Find the distance between two points distance(a, b) { let x = b.x - a.x; let y = b.y - a.y; return Math.sqrt(x * x + y * y); }, ctaCalc(x0, y0, r0, x1, y1, r1, x2, y2, r2) { let a = [], vx01, vy01, norm, n_x0, n_y0, vx21, vy21, n_x2, n_y2; vx01 = x1 - x0; vy01 = y1 - y0; norm = Math.sqrt(vx01 * vx01 + vy01 * vy01 + 0.0001) * 2; vx01 = vx01 / norm * r0; vy01 = vy01 / norm * r0; n_x0 = vy01; n_y0 = -vx01; vx21 = x1 - x2; vy21 = y1 - y2; norm = Math.sqrt(vx21 * vx21 + vy21 * vy21 + 0.0001) * 2; vx21 = vx21 / norm * r2; vy21 = vy21 / norm * r2; n_x2 = -vy21; n_y2 = vx21; a.push({ mx: x0 + n_x0, my: y0 + n_y0, color: "#1A1A1A" }); 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 }); 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 }); 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 }); 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 }); a[0].mx = a[0].mx.toFixed(1); a[0].mx = parseFloat(a[0].mx); a[0].my = a[0].my.toFixed(1); a[0].my = parseFloat(a[0].my); for (let i = 1; i < a.length; i++) { a[i].c1x = a[i].c1x.toFixed(1); a[i].c1x = parseFloat(a[i].c1x); a[i].c1y = a[i].c1y.toFixed(1); a[i].c1y = parseFloat(a[i].c1y); a[i].c2x = a[i].c2x.toFixed(1); a[i].c2x = parseFloat(a[i].c2x); a[i].c2y = a[i].c2y.toFixed(1); a[i].c2y = parseFloat(a[i].c2y); a[i].ex = a[i].ex.toFixed(1); a[i].ex = parseFloat(a[i].ex); a[i].ey = a[i].ey.toFixed(1); a[i].ey = parseFloat(a[i].ey); } return a; }, bethelDraw(point, is_fill, color) { let ctx = this.data.ctx; ctx.beginPath(); ctx.moveTo(point[0].mx, point[0].my); if (undefined != color) { ctx.setFillStyle(color); ctx.setStrokeStyle(color); } else { ctx.setFillStyle(point[0].color); ctx.setStrokeStyle(point[0].color); } for (let i = 1; i < point.length; i++) { ctx.bezierCurveTo(point[i].c1x, point[i].c1y, point[i].c2x, point[i].c2y, point[i].ex, point[i].ey); } ctx.stroke(); if (undefined != is_fill) { ctx.fill(); //Fill graphics (the graphics drawn later will overwrite the previous graphics, and pay attention to the order when drawing) } ctx.draw(true) }, //complete subCanvas() { if (this.data.linePrack.length == 0) { util.showToast("No signature") } else { this.uploadCanvasImg(); } }, //upload uploadCanvasImg() { var that = this; // This is a prompt, dd.showloading ({content: Message}); md5.loading('Signature generation in progress...'); that.data.ctx.toTempFilePath({ success(res) { that.drowImage(res.filePath); } }) }, //Set the canvas background color. Do not set the background of the exported canvas to be transparent //@params: String color setCanvasBg(color) { /* Set the canvas background to white background, and do not set the exported canvas background to transparent */ //The rect() parameter describes the abscissa of the upper left corner of the rectangular path, the ordinate of the upper left corner, the width of the rectangular path, and the height of the rectangular path //Here is canvasHeight - 4, because the lower edge covers the border, so it is written down manually this.data.ctx.rect(0, 0, this.data.canvasWidth, this.data.canvasHeight - 4); // ctx.setFillStyle('red') this.data.ctx.setFillStyle(color) this.data.ctx.fill() //Set fill this.data.ctx.draw() //Open painting }, drowImage(path) { //This path is the temporary path of the first canvas. Bring it here var canvasContext = my.createCanvasContext('canvas')//Unique identification of the second canvas canvasContext.scale(0.5, 0.5);//Double the size of the first canvas, because my first one is relatively large and can be adjusted by myself. It is too large and the display is incomplete canvasContext.translate(0, 300)//This is the origin offset (the height of the second canvas is 300 and the width is 600) canvasContext.rotate(-90 * Math.PI / 180)//Rotate 90 ° counterclockwise to get the horizontal signature canvasContext.drawImage(path, 0, 0, 300, 600)//Draw the first one on the second canvasContext.draw(true)//Note that it takes time to draw this place. You need to delay obtaining the temporary picture below, otherwise you may get blank because the draw() method is not completed setTimeout(() => { //Convert the second Canvas content to a temporary picture canvasContext.toTempFilePath({ success(res) { console.log("AAAAAFFFFF" + res.filePath) //Upload pictures dd.uploadFile({ url: 'https://wms.api.lancjt.com/api/Qiniuupload/upload', filePath: res.filePath, fileType: 'image', fileName: 'file', header: { 'content-type': 'multipart/form-data' }, success: (res) => { // res.data is the relevant data returned by your backend const data = JSON.parse(res.data) var urls = data.data var pages = getCurrentPages(); // Get page stack var prevPage = pages[pages.length - 2]; // Previous page console.log("AAAAAFFFFF" + urls) md5.gone(); prevPage.setData({ qian: urls // False data }) my.navigateBack({//return delta: 1 }) }, fial: (res) => { console.log("AAAAAFFFFF" + res) } }) } }) }, 1000) }, /** * Life cycle function -- listening for page loading */ onLoad: function (options) { let canvasName = this.data.canvasName let ctx = my.createCanvasContext(canvasName) this.setData({ ctx: ctx }) var query = my.createSelectorQuery(); query.select('.handCenter').boundingClientRect(rect => { this.setData({ canvasWidth: rect.width, canvasHeight: rect.height }) /* Set the canvas background to white background, and do not set the exported canvas background to transparent */ // console.log(this, 'hahah'); this.setCanvasBg('#fff'); }).exec(); },
####Layout style
page { background: #fbfbfb; height: auto; overflow: hidden; } .wrapper { width: 100%; height: 95vh; margin: 30rpx 0; overflow: hidden; display: flex; align-content: center; flex-direction: row; justify-content: center; font-size: 28rpx; margin-left: 20rpx; } //First canvas .handWriting { position: relative; background: #fff; width: 98%; z-index: 99; height: 83vh; border: 4rpx dashed #e9e9e9; } .ats{ position: relative; } //Second canvas .handWriting2{ position: absolute; z-index: 9; width: 600rpx; height: 300rpx; top: 0; left: 0; /* transform: rotate(90deg); */ background-color: #FFF; } .handRight { display: flex; height: 100%; align-items: center; justify-content: center; text-align: center; width: 100rpx; } .handCenter { display: flex; width: 620rpx; flex-direction: column; overflow: hidden; box-sizing: border-box; } .handTitle { display: flex; color: #000000; font-size: 33rpx; font-weight: bold; transform: rotate(90deg); align-items: center; justify-content: center; text-align: center; } .handBtn button { font-size: 28rpx; } .handBtn { display: inline-flex; flex-direction: row; justify-content: space-between; align-content: space-between; text-align: center; flex: 1; } .delBtn { display: flex; width: 120rpx; height: 75rpx; transform: rotate(90deg); color: #666; align-items: center; justify-content: center; margin-top: 60rpx; } .delBtn image { position: absolute; top: 13rpx; left: 25rpx; } .subBtn { display: flex; width: 120rpx; height: 75rpx; display: inline-flex; transform: rotate(90deg); background: linear-gradient(133deg, #5E69FF 0%, #2E37A8 100%); box-shadow: 0rpx 6rpx 12rpx rgba(0, 0, 0, 0.16); opacity: 1; border-radius: 13rpx; color: #fff; align-items: center; justify-content: center; margin-top: 60rpx; }