目录
- 1.前言
- 2.多功能画板的实现
- 2.1 画板初始化
- 2.2 画笔
- 2.3 橡皮擦
- 2.4 清屏
- 2.5 前进和后退
- 3.小结
1.前言
HTML5提供的Canvas标签能实现很多有趣的效果,本文就来分享一下如何使用Canvas来实现一个极简的多功能画板。先来看效果:
主要实现以下功能:
- 画笔
- 橡皮擦
- 清屏
- 前进
- 后退
下面就来一步步实现。
2.多功能画板的实现
2.1 画板初始化
首先,准备一个canvas画板容器,后续所有的操作都将在这个容器上进行绘制。
<!-- 画板容器 -->
<canvas id="canvas"></canvas>
<!-- 参数配置栏,样式可以自行定义 -->
<div class="toolBar">
<p><b>画笔</b></p>
<div>
<span>颜色:</span>
<input type="color" id="colorSelect" />
</div>
<div>
<span>宽度:</span>
<input type="range" min="1" max="30" value="1" id="widthRange" />
<span id="widthValue">1</span>
</div>
<p><b>工具栏</b></p>
<div class="tool">
<button class="btn" id="eraser">橡皮擦</button>
<button class="btn" id="clear">清屏</button>
<button class="btn" id="undo">后退</button>
<button class="btn" id="redo">前进</button>
</div>
</div>
获取二维绘图渲染上下文
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
画板初始化:这里主要做两件事,一个是设置画布为全屏,一个是初始化线条的样式。
//样式配置
const config = {
lineColor: "#000",//线条颜色
lineStyle: "round",//线段端点样式
lineWidth: 1,//线宽
};
function initCanvas() {
//设置画布为全屏
const pageWidth = document.documentElement.clientWidth;
const pageHeight = document.documentElement.clientHeight;
canvas.width = pageWidth;
canvas.height = pageHeight;
//初始化线条样式
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.lineWidth = 1;
ctx.strokeStyle = "#000";
}
注意,这里设置了lineCap和lineJoin为round,就是让线段末端以及两线段连接处都为圆形,可以实现更自然的画笔效果。
如上图所示,上面一行是默认的效果,下面一行是设置为round后的效果。
2.2 画笔
先封装画点和画线的两个方法
//画点
function drawPoint(x, y) {
ctx.beginPath();
ctx.arc(x, y, ctx.lineWidth / 2, 0, 2 * Math.PI, false);
ctx.fill();
}
//画线
function drawLine({ x1, y1, x2, y2 }) {
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
接下来需要监听鼠标事件,在鼠标移动过程中记录鼠标坐标位置,通过drawLine()方法进行绘制。
//记录画笔最后一次的位置
let lastPoint = null;
function listenEvent() {
//鼠标按下事件
canvas.addEventListener("mousedown", (e) => {
const x = e.clientX;
const y = e.clientY;
drawPoint(x, y);//鼠标按下就画一个点
lastPoint = { x, y };//记录每次按下时点的位置
//鼠标移动事件
canvas.addEventListener("mousemove", moveDraw);
//鼠标松开事件
canvas.addEventListener("mouseup", (e) => {
canvas.removeEventListener("mousemove", moveDraw);
});
});
}
//移动时不断画线和记录鼠标位置
function moveDraw(e) {
const x2 = e.clientX;
const y2 = e.clientY;
drawLine({ ...lastPoint, x2, y2 });
lastPoint = { x: x2, y: y2 };
}
现在就已经初步实现一个画笔的效果:
接着实现画笔颜色和宽度的动态设置,只需监听颜色选择器和宽度的input事件即可,发生变化时重新赋值。
const colorSelect = document.getElementById("colorSelect");
const widthRange = document.getElementById("widthRange");
const widthValue = document.getElementById("widthValue");
function listenEvent() {
//鼠标按下事件
canvas.addEventListener("mousedown", (e) => {...});
//监听颜色选择器变化
colorSelect.addEventListener("input", function () {
ctx.strokeStyle = this.value;
});
//监听宽度变化
widthRange.addEventListener("input", function () {
widthValue.textContent = this.value;
ctx.lineWidth = this.value;
});
}
效果如下:
2.3 橡皮擦
实现思路很简单,点击橡皮擦时,直接让之后绘制的线条颜色与画板背景色保持一致即可,并且可以设置橡皮擦即线条的宽度,但是有一点要注意,当再次切换为画笔即选择颜色时,需要重新设置线条宽度。
const eraser = document.getElementById("eraser");
function listenEvent() {
...
colorSelect.addEventListener("input", function () {
...
ctx.lineWidth = widthRange.value; //从橡皮擦切换回画笔时需要重新设置宽度
});
...
//橡皮擦
eraser.addEventListener("click", () => {
ctx.strokeStyle = "#fff";
ctx.lineWidth = 5;
});
}
来看效果:
2.4 清屏
清屏的实现思路很简单,直接调用clearRect方法设置所有像素都是透明即可。
const clear = document.getElementById("clear");
function listenEvent() {
...
//清屏
clear.addEventListener("click", () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
});
}
2.5 前进和后退
前进和后退的实现思路:利用两个数组来分别保存绘制的记录和撤销的记录,当点击后退(撤销)时,将绘制数组中最后一条记录转移到撤销记录数组中,当点击前进(重做)时,将撤销数组中最后一条记录重新转移到绘制数组中,然后遍历绘制数组进行重绘即可。
首先定义两个数组:drawData数组——保存绘制的记录;revokedData数组——保存撤销的记录。
const drawData = []; //保存绘制的记录
const revokedData = []; //保存撤销的记录
每次绘制时需要保存当前线段的信息:起始点,坐标位置数组,颜色,线宽。
//记录线段信息
function recordInfo(type, data) {
switch (type) {
case "moveTo":
drawData.push({
moveTo: [...data],
lineTo: [],
color: ctx.strokeStyle,
width: ctx.lineWidth,
});
break;
case "lineTo":
drawData[drawData.length - 1]["lineTo"].push([...data]);
break;
default:
break;
}
}
canvas.addEventListener("mousedown", (e) => {
const x = e.clientX;
const y = e.clientY;
lastPoint = { x, y };
//记录每个线段起始位置
recordInfo("moveTo", [x, y]);
drawPoint(x, y);
canvas.addEventListener("mousemove", moveDraw);
canvas.addEventListener("mouseup", (e) => {
canvas.removeEventListener("mousemove", moveDraw);
});
});
function moveDraw(e) {
const x2 = e.clientX;
const y2 = e.clientY;
drawLine({ ...lastPoint, x2, y2 });
//记录每个线段除起始点外的位置
recordInfo("lineTo", [x2, y2]);
lastPoint = { x: x2, y: y2 };
}
- 后退:将drawData绘制数组中最后一条记录转移到revokedData撤销记录数组中,遍历drawData进行重绘。
- 前进:将revokedData撤销数组中最后一条记录重新转移到drawData绘制数组中,遍历drawData进行重绘。
function listenEvent() {
...
//后退(撤销)
undo.addEventListener("click", () => {
//把绘制的最后一条记录放入撤销的容器中
drawData.length > 0 && revokedData.push(drawData.pop());
//重绘
reDraw();
//当有一个为空时,需要重新设置颜色和宽度
if (!drawData.length || !revokedData.length) {
ctx.strokeStyle = colorSelect.value;
ctx.lineWidth = widthRange.value;
}
});
//前进(重做)
redo.addEventListener("click", () => {
//把撤销的容器中最后一条记录放入需要绘制的容器中
revokedData.length > 0 && drawData.push(revokedData.pop());
//重绘
reDraw();
//当有一个为空时,需要重新设置颜色和宽度
if (!drawData.length || !revokedData.length) {
ctx.strokeStyle = colorSelect.value;
ctx.lineWidth = widthRange.value;
}
});
}
//取出drawData中保存的数据进行一一绘制
function reDraw() {
//重绘前清空画布
ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight);
//重绘
drawData.forEach((item) => {
ctx.beginPath();
const { moveTo, lineTo, color, width } = item;
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.moveTo(...moveTo);
lineTo.forEach((line) => {
ctx.lineTo(...line);
});
ctx.stroke();
});
}
完整代码如下:
const drawData = []; //保存绘制的记录
const revokedData = []; //保存撤销的记录
function listenEvent() {
//鼠标按下事件
canvas.addEventListener("mousedown", (e) => {
const x = e.clientX;
const y = e.clientY;
lastPoint = { x, y };
recordInfo("moveTo", [x, y]);
drawPoint(x, y);
//鼠标移动事件
canvas.addEventListener("mousemove", moveDraw);
//鼠标松开事件
canvas.addEventListener("mouseup", (e) => {
canvas.removeEventListener("mousemove", moveDraw);
});
});
...
//后退(撤销)
undo.addEventListener("click", () => {
drawData.length > 0 && revokedData.push(drawData.pop());
reDraw();
if (!drawData.length || !revokedData.length) {
ctx.strokeStyle = colorSelect.value;
ctx.lineWidth = widthRange.value;
}
});
//前进(重做)
redo.addEventListener("click", () => {
revokedData.length > 0 && drawData.push(revokedData.pop());
reDraw();
if (!drawData.length || !revokedData.length) {
ctx.strokeStyle = colorSelect.value;
ctx.lineWidth = widthRange.value;
}
});
}
function moveDraw(e) {
const x2 = e.clientX;
const y2 = e.clientY;
drawLine({ ...lastPoint, x2, y2 });
recordInfo("lineTo", [x2, y2]);
lastPoint = { x: x2, y: y2 };
}
//重绘
function reDraw() {
ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight);
drawData.forEach((item) => {
ctx.beginPath();
const { moveTo, lineTo, color, width } = item;
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.moveTo(...moveTo);
lineTo.forEach((line) => {
ctx.lineTo(...line);
});
ctx.stroke();
});
}
//记录线段信息
function recordInfo(type, data) {
switch (type) {
case "moveTo":
drawData.push({
moveTo: [...data],
lineTo: [],
color: ctx.strokeStyle,
width: ctx.lineWidth,
});
break;
case "lineTo":
drawData[drawData.length - 1]["lineTo"].push([...data]);
break;
default:
break;
}
}
现在让我们来看下前进和后退的效果:
3.小结
本文主要实现了一个极简的Canvas多功能画板,还有很多功能没写上,如多层图、保存等,后续可以继续完善。
以上就是本文的全部分享了,如有问题,欢迎指出,如有帮助,点个赞,鼓励一下作者吧!