sign.js
const env = require("../../env");
Component({
data: {
FILE_SERVER: env.FILE_SERVER,
canvasName: 'handWriting',
canvas: null,
ctx: null,
canvasWidth: 0,
canvasHeight: 0,
transparent: 1, // 透明度
selectColor: 'black',
lineColor: '#1A1A1A', // 颜色
lineSize: 1.5, // 笔记倍数
lineMin: 0.5, // 最小笔画半径
lineMax: 4, // 最大笔画半径
pressure: 1, // 默认压力
smoothness: 60, // 顺滑度,用60的距离来计算速度
currentPoint: {},
currentLine: [], // 当前线条
firstTouch: true, // 第一次触发
radius: 1, // 画圆的半径
cutArea: {
top: 0,
right: 0,
bottom: 0,
left: 0
}, // 裁剪区域
bethelPoint: [], // 保存所有线条生成的贝塞尔点;
lastPoint: 0,
chirography: [], // 笔迹
currentChirography: {}, // 当前笔迹
linePrack: [] // 划线轨迹 , 生成线条的实际点
},
methods: {
selectColorEvent(e) {
const color = e.currentTarget.dataset.color;
const hex = e.currentTarget.dataset.hex;
this.setData({
selectColor: color,
lineColor: hex
});
},
//画笔开始
uploadScaleStart(e) {
if (e.type !== 'touchstart') return false;
const {
ctx,
lineColor,
transparent,
currentLine,
firstTouch
} = this.data;
//初始线条设置颜色
ctx.fillStyle = lineColor
// 设置半透明
ctx.globalAlpha = transparent
const currentPoint = {
x: e.touches[0].x,
y: e.touches[0].y
};
currentLine.unshift({
time: new Date().getTime(),
dis: 0,
x: currentPoint.x,
y: currentPoint.y
});
this.setData({
currentPoint
});
if (firstTouch) {
this.setData({
cutArea: {
top: currentPoint.y,
right: currentPoint.x,
bottom: currentPoint.y,
left: currentPoint.x
},
firstTouch: false
});
}
//点划线
this.pointToLine(currentLine);
},
//画笔移动
uploadScaleMove(e) {
if (e.type !== 'touchmove') return false;
if (e.cancelable && !e.defaultPrevented) {
e.preventDefault();
}
const point = {
x: e.touches[0].x,
y: e.touches[0].y
};
// 更新裁剪区域
const cutArea = this.data.cutArea;
if (point.y < cutArea.top) cutArea.top = point.y;
if (point.x > cutArea.right) cutArea.right = point.x;
if (point.y > cutArea.bottom) cutArea.bottom = point.y;
if (point.x < cutArea.left) cutArea.left = point.x;
this.setData({
cutArea,
lastPoint: this.data.currentPoint,
currentPoint: point
});
const 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.pointToLine(currentLine);
},
//画笔结束
uploadScaleEnd(e) {
if (e.type !== 'touchend') return;
const point = {
x: e.changedTouches[0].x,
y: e.changedTouches[0].y
};
this.setData({
lastPoint: this.data.currentPoint,
currentPoint: point
});
const 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.pointToLine(currentLine);
const currentChirography = {
lineSize: this.data.lineSize,
lineColor: this.data.lineColor
};
const chirography = this.data.chirography;
chirography.unshift(currentChirography);
this.setData({
chirography,
linePrack: [currentLine, ...this.data.linePrack],
currentLine: []
});
},
//有点画线
pointToLine(line) {
this.calcBethelLine(line);
},
// 计算贝塞尔曲线
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;
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;
}
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.data.radius = Math.min((time / len) * this.data.pressure + this.data.lineMin, this.data.lineMax) * this.data.lineSize;
line[0].r = this.data.radius;
if (line.length <= 2) {
r0 = (lastRadius + this.data.radius) / 2;
r1 = r0;
r2 = r1;
} 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,
y,
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;
this.bethelDraw(a, 1);
point = [{
x,
y,
r
}];
}
}
this.setData({
currentLine: line
});
},
// 求两点之间距离
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 = parseFloat(a[0].mx.toFixed(1));
a[0].my = parseFloat(a[0].my.toFixed(1));
for (let i = 1; i < a.length; i++) {
a[i].c1x = parseFloat(a[i].c1x.toFixed(1));
a[i].c1y = parseFloat(a[i].c1y.toFixed(1));
a[i].c2x = parseFloat(a[i].c2x.toFixed(1));
a[i].c2y = parseFloat(a[i].c2y.toFixed(1));
a[i].ex = parseFloat(a[i].ex.toFixed(1));
a[i].ey = parseFloat(a[i].ey.toFixed(1));
}
return a;
},
// 绘制贝塞尔曲线
bethelDraw(point, is_fill, color) {
let ctx = this.data.ctx;
ctx.beginPath();
ctx.moveTo(point[0].mx, point[0].my);
ctx.fillStyle = color || point[0].color
ctx.strokeStyle = color || 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 (is_fill) {
ctx.fill(); // 填充图形
}
},
//设置canvas背景色默认导出的canvas的背景为透明
setCanvasBg(color) {
const {
ctx,
canvasWidth,
canvasHeight
} = this.data;
ctx.rect(0, 0, canvasWidth, canvasHeight);
ctx.fillStyle = color
ctx.fill();
},
//画布重置-重画
retDraw() {
const ctx = this.data?.ctx;
ctx.clearRect(0, 0, 700, 730);
ctx.draw();
// 设置 canvas 背景
this.setCanvasBg('#fff');
},
//完成
subCanvas() {
const _this = this;
wx.canvasToTempFilePath({
canvas: this.data.canvas,
fileType: 'png',
quality: 1, //图片质量
success(res) {
// 旋转图片并重新生成图片地址
_this.rotateAndSaveImage(res.tempFilePath);
},
complete(res) {
console.log(res, 'rse')
}
});
},
// 旋转图片并保存
rotateAndSaveImage(tempFilePath) {
const _this = this;
const query = wx.createSelectorQuery().in(this);
// 获取canvas节点
query.select('#myCanvas_roreate')
.fields({
node: true,
size: true
})
.exec((res) => {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
// 设置画布大小为旋转后的宽高
const canvasWidth = res[0].height;
const canvasHeight = res[0].width;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
// 旋转 90 度
ctx.translate(canvasWidth / 2, canvasHeight / 2); // 移动到中心点
ctx.rotate(-Math.PI / 2); // 旋转 90 度
ctx.translate(-canvasHeight / 2, -canvasWidth / 2); // 移动回去
ctx.fillStyle = '#fff'
// 将原始图片绘制到旋转后的 canvas
const img = canvas.createImage();
img.src = tempFilePath;
img.onload = () => {
ctx.drawImage(img, 0, 0, canvasHeight, canvasWidth);
// 生成新的图片地址
wx.canvasToTempFilePath({
canvas: canvas,
fileType: 'png',
quality: 1,
success(res) {
_this.triggerEvent('signComplate',res.tempFilePath)
},
fail(err) {
console.error('生成旋转图片失败:', err);
}
});
};
});
},
},
lifetimes: {
attached: function () {
const _this = this;
const query = wx.createSelectorQuery().in(this)
query.select('#myCanvas')
.fields({
node: true,
size: true
})
.exec((res) => {
const resDataFirstObj = res[0] || {};
const canvas = resDataFirstObj.node;
const ctx = canvas.getContext('2d');
const dpr = wx.getSystemInfoSync().pixelRatio;
canvas.width = resDataFirstObj.width * dpr;
canvas.height = resDataFirstObj.height * dpr;
ctx.scale(dpr, dpr);
_this.setData({
ctx,
canvas,
canvasWidth: canvas.width,
canvasHeight: canvas.height
});
_this.setCanvasBg('#fff');
})
},
}
});
index.scss
.wrapper {
display: flex;
align-content: center;
flex-direction: row;
justify-content: center;
overflow: hidden;
box-sizing: border-box;
width: 100%;
height: 100vh;
padding: 30rpx 0;
font-size: 28rpx;
//左侧模块
.handLeft {
height: 95vh;
display: inline-flex;
flex-direction: column;
justify-content: space-between;
align-content: space-between;
flex: 1;
// 画笔颜色选择
.penColor_box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.color_select {
width: 60rpx;
height: 60rpx;
}
.black-select.color_select {
width: 90rpx;
height: 90rpx;
}
.red-select.color_select {
width: 90rpx;
height: 90rpx;
}
}
// 功能按钮
.button_box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.baseBtn {
color: #666;
font-size: 28rpx;
padding: 60rpx 0rpx;
margin: 10px 0;
.text{
transform: rotate(90deg);
letter-spacing: 8rpx;
}
}
.delBtn {
color: #666;
}
.subBtn {
background: #008ef6;
color: #fff;
}
}
}
// canvas 中间模块
.handCenter {
border: 4rpx dashed #e9e9e9;
flex: 5;
overflow: hidden;
box-sizing: border-box;
.handWriting {
background: #fff;
width: 100%;
height: 100%;
}
}
//右侧模块
.handRight {
display: inline-flex;
align-items: center;
.handTitle {
transform: rotate(90deg);
flex: 1;
color: #666;
}
}
}
index.wxml
<view class="wrapper">
<!-- 左侧模块 -->
<view class="handLeft">
<!-- 画笔颜色选择 -->
<view class="penColor_box">
<image bindtap="selectColorEvent" data-color="black" data-hex="#1A1A1A" src="{{FILE_SERVER}}/common/{{ selectColor === 'black' ?'color_black_selected':'color_black' }}.png" class="{{selectColor === 'black' ? 'black-select' : ''}} color_select"></image>
<image bindtap="selectColorEvent" style="margin-top: 10rpx;" data-color="red" data-hex="#ca262a" src="{{FILE_SERVER}}/common/{{ selectColor === 'red' ? 'color_red_selected':'color_red' }}.png" class="{{selectColor === 'red' ? 'red-select' : ''}} color_select"></image>
</view>
<!-- 功能按钮 -->
<view class="button_box">
<button bindtap="retDraw" class="baseBtn delBtn"><view class="text">重写</view></button>
<button bindtap="subCanvas" class="baseBtn subBtn"><view class="text">完成</view></button>
</view>
</view>
<!-- 中间模块 -->
<view class="handCenter">
<canvas class="handWriting" disable-scroll="{{true}}" bindtouchstart="uploadScaleStart" bindtouchmove="uploadScaleMove" bindtouchend="uploadScaleEnd" type="2d" id="myCanvas"></canvas>
<canvas class="handWriting" disable-scroll="{{true}}" type="2d" id="myCanvas_roreate"></canvas>
</view>
<!-- 右侧模块 -->
<view class="handRight">
<view class="handTitle">请签名</view>
</view>
</view>