According to the handwritten signature component of Canvas...
Background
Due to business requirements, it is necessary to implement a handwritten signature component on the DingTalk Mini Program. After referring to the implementation of handwritten signatures in WeChat Mini Programs online and considering our own actual needs, we have wrapped it into a popup dialog style, while also referring to the handwritten signature style in DingTalk's approval process. The dependencies used include the mini-ali-ui
popup and button components, as well as a local icon.
{
"component": true,
"usingComponents": {
"popup": "mini-ali-ui-rpx/es/popup/index",
"button": "mini-ali-ui-rpx/es/button/index"
}
}
Approach
The specific implementation is based on the canvas. The canvas API on the Mini Program side is similar to that of WeChat, so this component should be able to be used on the WeChat side with slight modifications.
Some Issues
- The width and height of the canvas must be fixed, so the implementation is based on the actual width obtained multiplied by a fixed ratio for the height. This can be passed directly in the component.
- The required parameter
dpr
is referenced from How to Solve the Blurry Canvas Problem. - Perhaps I misunderstood, but the
save
andrestore
methods did not achieve the desired effect, so the undo function may need to be implemented in a different way. It has been removed here. clearRect
does not work unless a line ofcontent.beginPath()
is added before it. I cannot understand how this issue arises.
Attached: Comparison of clearRect Not Working
Implementation
<popup
show="{{show}}"
animation="{{animation}}"
position="{{position}}"
onClose="onCancel"
zIndex="{{zIndex}}"
>
<view class="box">
<slot name="toolbar"
><view class="toolbar">
<button type="text" onTap="onCancel">{{cancelButtonText}}</button>
<text a:if="{{title}}" class="title">{{title}}</text>
<button type="text" onTap="onConfirm">{{confirmButtonText}}</button>
</view></slot
>
<canvas
width="{{width*dpr}}"
height="{{height*dpr}}"
style="{{'width:'+(width-4)+'px;height:'+(height-4)+'px;'}}"
class="sign"
id="sign"
onTouchMove="move"
onTouchStart="start"
onTouchEnd="end"
onTouchCancel="cancel"
onLongTap="tap"
disable-scroll="{{true}}"
onTap="tap"
>
</canvas>
<image
class="clear-icon"
src="/icon/icon_clear.svg"
onTap="clearClick"
></image>
<!--适应底部-->
<view style="height: 24rpx"></view>
</view>
</popup>
import fmtEvent from "mini-ali-ui-rpx/es/_util/fmtEvent";
const noop = function noop() {};
Component({
mixins: [],
data: {
ctx: null,
points: [],
signImage: "",
},
props: {
show: false,
animation: true,
zIndex: 100,
title: "手写签名",
cancelButtonText: "取消",
confirmButtonText: "完成",
onCancel: noop,
onConfirm: noop,
width: 300,
height: 225,
dpr: 2,
},
didMount() {
//获得Canvas的上下文
this.data.ctx = dd.createCanvasContext("sign");
//设置线的颜色
this.data.ctx.setStrokeStyle("#000");
//设置线的宽度
this.data.ctx.setLineWidth(3);
//设置线两端端点样式更加圆润
this.data.ctx.setLineCap("round");
//设置两条线连接处更加圆润
this.data.ctx.setLineJoin("round");
this.data.ctx.scale(this.props.dpr, this.props.dpr);
this.data.ctx.save();
},
didUpdate() {},
didUnmount() {},
methods: {
// 画布的触摸移动开始手势响应
start(e) {
//获取触摸开始的 x,y
// this.data.ctx.save()
console.log(e);
let point = { x: e.changedTouches[0].x, y: e.changedTouches[0].y };
this.$spliceData({ points: [0, 0, point] });
},
// 画布的触摸移动手势响应
move(e) {
let point = { x: e.touches[0].x, y: e.touches[0].y };
this.$spliceData({ points: [this.data.points.length, 0, point] });
if (this.data.points.length >= 2) {
this.draw();
}
},
// 画布的触摸移动结束手势响应
end(e) {
// console.log("触摸结束",e);
//清空轨迹数组
this.setData({
points: [],
});
this.data.ctx.save();
},
// 画布的触摸取消响应
cancel(e) {
console.log("触摸取消" + e);
},
// 画布的长按手势响应
tap(e) {
console.log("长按手势", e);
},
error(e) {
console.log("画布触摸错误" + e);
},
//绘制
draw() {
let point1 = this.data.points[0];
let point2 = this.data.points[1];
this.$spliceData({ points: [0, 1] });
this.data.ctx.moveTo(point1.x, point1.y);
this.data.ctx.lineTo(point2.x, point2.y);
this.data.ctx.stroke();
this.data.ctx.draw();
},
//清除操作
clearClick() {
//清除画布
// console.log(this.data.points)
this.data.ctx.save();
//非常重要,没有就会有问题
this.data.ctx.beginPath();
this.data.ctx.clearRect(0, 0, this.props.width, this.props.height);
this.data.ctx.draw();
},
//保存图片
saveClick() {
const that = this;
this.data.ctx.toTempFilePath({
success(res) {
console.log(res);
that.setData({
signImage: res.filePath,
});
},
});
},
onCancel(e) {
const event = fmtEvent(this.props, e);
this.props.onCancel(event);
},
onConfirm(e) {
this.saveClick();
e.detail.value = this.data.signImage;
const event = fmtEvent(this.props, e);
this.props.onConfirm(event);
},
},
});
.sign {
box-sizing: border-box;
border: 2px #bbbbbb dashed;
}
.box {
background: #fff;
position: relative;
}
.toolbar {
padding: 0 24rpx;
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
}
.am-button {
font-size: 28rpx;
}
.title {
font-weight: 600;
}
.clear-icon {
position: absolute;
right: 40rpx;
bottom: 80rpx;
width: 66rpx;
height: 66rpx;
}
This post is translated using ChatGPT, please feedback if any omissions.