150行代码实现一个极简的Canvas多功能画板

前言

大家好,我是南木元元,热衷分享有趣实用的文章。HTML5提供的Canvas标签能实现很多有趣的效果,本文就来分享一下如何使用Canvas来实现一个极简的多功能画板。

演示效果

话不多说,先来看完成后的效果:

在这里插入图片描述

主要实现以下功能:

  1. 画笔
  2. 橡皮擦
  3. 清屏
  4. 前进
  5. 后退

下面我们就来一步步实现。

多功能画板的实现

画板初始化

首先,准备一个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");

画板初始化:这里主要做两件事,一个是设置画布为全屏,一个是初始化线条的样式。

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后的效果。

画笔

先封装画点和画线的两个方法

//画点
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;
  });
}

效果如下:
在这里插入图片描述

橡皮擦

实现思路很简单,点击橡皮擦时,直接让之后绘制的线条颜色与画板背景色保持一致即可,并且可以设置橡皮擦即线条的宽度,但是有一点要注意,当再次切换为画笔即选择颜色时,需要重新设置线条宽度。

const eraser = document.getElementById("eraser");

function listenEvent() {
  ...
  
  colorSelect.addEventListener("input", function () {
    ...
    ctx.lineWidth = widthRange.value; //从橡皮擦切换回画笔时需要重新设置宽度
  });

  ...
  
  //橡皮擦
  eraser.addEventListener("click", () => {
    ctx.strokeStyle = "#fff";
    ctx.lineWidth = 5;
  });
}

来看效果:
在这里插入图片描述

清屏

清屏的实现思路很简单,直接调用clearRect方法设置所有像素都是透明即可。

const clear = document.getElementById("clear");

function listenEvent() {
  ...
  //清屏
  clear.addEventListener("click", () => {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
  });
}

前进和后退

前进和后退的实现思路:利用两个数组来分别保存绘制的记录和撤销的记录,当点击后退(撤销)时,将绘制数组中最后一条记录转移到撤销记录数组中,当点击前进(重做)时,将撤销数组中最后一条记录重新转移到绘制数组中,然后遍历绘制数组进行重绘即可。

首先定义两个数组: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;
  }
}

现在让我们来看下前进和后退的效果:
在这里插入图片描述

结语

本文主要实现了一个极简的Canvas多功能画板,还有很多功能没写上,如多层图、保存等,后续可以继续完善。

🔥如果此文对你有帮助的话,欢迎💗关注、👍点赞、⭐收藏、✍️评论,支持一下博主~

  • 137
    点赞
  • 119
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 202
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 202
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南木元元

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值