插件文档地址:https://www.npmjs.com/package/perfect-freehand
插件体验地址:https://perfect-freehand-example.vercel.app/
这个包导出一个名为getStroke
的函数,该函数将根据鼠标点数组转变为多边形生成点.。
文末贴有完整组件的代码
1.数据准备
<template>
<div>
<canvas
ref="canvas"
@pointerdown="handlePointerDown"
@pointermove="handlePointerMove"
@pointerup="handlePointerUp"
/>
<div>
颜色:
<el-input
v-model="color"
style="width: 100px"
type="color"
@change="changeColor"
/>
粗细:<el-select
v-model="size"
style="width: 100px"
placeholder="请选择"
@change="changeSize"
>
<el-option
v-for="item in [4, 8, 16, 24]"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<el-button @click="reset">重置</el-button>
<el-button @click="save">保存</el-button>
<el-button @click="revoke">撤销</el-button>
</div>
</div>
</template>
<script>
import { getStroke } from "perfect-freehand";
import getSvgPathFromStroke from "./util";
const SIZE = 8; //画笔大小
const COLOR = "#000"; //画笔颜色
export default {
data() {
return {
size: SIZE,
color: COLOR,
isDrawing: false,
points: [], //当前绘制中的坐标点
path: null, // 当前绘制中的线条路径
pathAll: [], //所有绘制的坐标点
canvas: null,
ctx: null,
options: {
size: SIZE,
thinning: 0.5,
smoothing: 0.5,
streamline: 0.5,
},
};
},
mounted() {
this.canvas = this.$refs.canvas;
this.canvas.width = 500;
this.canvas.height = 300;
this.ctx = this.canvas.getContext("2d");
},
methods: {
handlePointerDown(e) {
},
handlePointerMove(e) {
},
handlePointerUp() {
},
// 重新渲染画布
reRender() {
},
// 重置
reset() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.pathAll = [];
this.points = [];
this.size = SIZE;
this.color = COLOR;
this.options.size = this.size;
},
// 撤销
revoke() {
},
// 保存
save() {
const dataURL = this.canvas.toDataURL("image/png");
console.log("dataURL: ", dataURL);
},
changeColor(value) {
this.color = value;
},
changeSize(value) {
this.options.size = value;
},
},
};
</script>
<style>
canvas {
border: 1px solid #000;
}
</style>
2.获取鼠标点,转换为路径点
2-1.鼠标移入点击设置初始点
handlePointerDown(e) {
this.isDrawing = true;
e.target.setPointerCapture(e.pointerId);
this.points = [[e.offsetX, e.offsetY, e.pressure]];
},
2-2.鼠标开始滑动时,绘制路径 [核心操作]
思路:获取鼠标在canvas的点,调用插件提供getStroke方法获取画笔坐标点,调用getSvgPathFromStroke方法转换成path数据绘制 (getSvgPathFromStroke方法在插件官网有提供)
handlePointerMove(e) {
if (e.buttons !== 1) return;
this.points.push([e.offsetX, e.offsetY, e.pressure]);
this.ctx.fillStyle = this.color; //设置当前画笔颜色
const stroke = getStroke(this.points, this.options);
const pathData = getSvgPathFromStroke(stroke);
const myPath = new Path2D(pathData);
this.ctx.fill(myPath);
},
2-3.鼠标抬起
handlePointerUp() {
this.isDrawing = false
},
以上方法可以进行简单的绘画操作
如果不在乎锯齿的,以上的方法可以凑合当一个电子签名来用啦!
3.修复锯齿
就是在handlePointerMove中绘制的时候,清除上一次的操作
methods:{
handlePointerMove(e) {
if (e.buttons !== 1) return;
this.points.push([e.offsetX, e.offsetY, e.pressure]);
// 防止锯齿==start 1.清除画布 2.重新渲染直接的路径
this.reRender();
// ==end
this.ctx.fillStyle = this.color; //设置当前画笔颜色
const stroke = getStroke(this.points, this.options);
const pathData = getSvgPathFromStroke(stroke);
const myPath = new Path2D(pathData);
this.ctx.fill(myPath);
},
// 重新渲染画布
reRender() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
这时候你会发现,锯齿是没有了,但是你上一次的记录也同样在下一次绘画的时候,也被清除了,所以我们需要保存一下之前的绘画path数据
4.保存之前绘画数据
methods:{
handlePointerMove(e){
//...之前的代码
// 保存一下path
this.path = myPath;
},
handlePointerUp() {
//...之前的代码
this.pathAll.push({
path: this.path,
color: this.color,
});
},
}
4-1.在绘制的时候,重新渲染一下pathAll的数据
// 重新渲染画布
reRender() {
//...之前代码
this.pathAll.forEach((item) => {
this.ctx.fillStyle = item.color; //设置画笔颜色
this.ctx.fill(item.path); //填充路径
});
},
到目前为止,就可以渲染一个流畅的电子签字板啦!
5.更改画笔颜色
其实前面写的时候有加入color的字段,只需要我们更改一下color的变量即可
changeColor(value) {
this.color = value;
},
6.更改画笔大小
这个其实插件给出了变量option中的size,option里面还有还多字段,具体可以看官网
changeSize(value) {
this.options.size = value;
},
7.绘制内容转为图片数据
// 保存
save() {
const dataURL = this.canvas.toDataURL("image/png");
console.log("dataURL: ", dataURL);
},
8.撤销
前面我们有存储过页面所有绘制path的数据,撤销只需要讲path数组的最后一个去掉,然后重新渲染一次画布
// 撤销
revoke() {
if (this.pathAll.length == 0) return;
this.pathAll.pop();
this.reRender();
},
9.重置
数据的全部初始化
// 重置
reset() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.pathAll = [];
this.points = [];
this.size = SIZE;
this.color = COLOR;
this.options.size = this.size;
},
以上就是一个完整的绘制面板代码思路,
10.完整版代码
<template>
<div>
<canvas
ref="canvas"
@pointerdown="handlePointerDown"
@pointermove="handlePointerMove"
@pointerup="handlePointerUp"
/>
<div>
颜色:
<el-input
v-model="color"
style="width: 100px"
type="color"
@change="changeColor"
/>
粗细:<el-select
v-model="size"
style="width: 100px"
placeholder="请选择"
@change="changeSize"
>
<el-option
v-for="item in [4, 8, 16, 24]"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<el-button @click="reset">重置</el-button>
<el-button @click="save">保存</el-button>
<el-button @click="revoke">撤销</el-button>
</div>
</div>
</template>
<script>
import { getStroke } from "perfect-freehand";
import getSvgPathFromStroke from "./util";
const SIZE = 8; //画笔大小
const COLOR = "#000"; //画笔颜色
export default {
data() {
return {
size: SIZE,
color: COLOR,
isDrawing: false,
points: [], //当前绘制中的坐标点
path: null, // 当前绘制中的线条路径
pathAll: [], //所有绘制的坐标点
canvas: null,
ctx: null,
options: {
size: SIZE,
thinning: 0.5,
smoothing: 0.5,
streamline: 0.5,
},
};
},
mounted() {
this.canvas = this.$refs.canvas;
this.canvas.width = 500;
this.canvas.height = 300;
this.ctx = this.canvas.getContext("2d");
},
methods: {
handlePointerDown(e) {
this.isDrawing = true;
e.target.setPointerCapture(e.pointerId);
this.points = [[e.offsetX, e.offsetY, e.pressure]];
},
handlePointerMove(e) {
if (e.buttons !== 1) return;
this.points.push([e.offsetX, e.offsetY, e.pressure]);
// 防止锯齿==start 1.清除画布 2.重新渲染直接的路径
this.reRender();
// ==end
this.ctx.fillStyle = this.color; //设置当前画笔颜色
const stroke = getStroke(this.points, this.options);
const pathData = getSvgPathFromStroke(stroke);
const myPath = new Path2D(pathData);
this.ctx.fill(myPath);
// 保存一下path
this.path = myPath;
},
handlePointerUp() {
this.isDrawing = false;
this.pathAll.push({
path: this.path,
color: this.color,
});
},
// 重新渲染画布
reRender() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.pathAll.forEach((item) => {
this.ctx.fillStyle = item.color; //设置画笔颜色
this.ctx.fill(item.path); //填充路径
});
},
// 重置
reset() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.pathAll = [];
this.points = [];
this.size = SIZE;
this.color = COLOR;
this.options.size = this.size;
},
// 撤销
revoke() {
if (this.pathAll.length == 0) return;
this.pathAll.pop();
this.reRender();
},
// 保存
save() {
const dataURL = this.canvas.toDataURL("image/png");
console.log("dataURL: ", dataURL);
},
changeColor(value) {
this.color = value;
},
changeSize(value) {
this.options.size = value;
},
},
};
</script>
<style>
canvas {
border: 1px solid #000;
}
</style>