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

目录

  • 1.前言
  • 2.多功能画板的实现
    • 2.1 画板初始化
    • 2.2 画笔
    • 2.3 橡皮擦
    • 2.4 清屏
    • 2.5 前进和后退
  • 3.小结

1.前言

HTML5提供的Canvas标签能实现很多有趣的效果,本文就来分享一下如何使用Canvas来实现一个极简的多功能画板。先来看效果:
在这里插入图片描述

主要实现以下功能:

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

下面就来一步步实现。

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多功能画板,还有很多功能没写上,如多层图、保存等,后续可以继续完善。

以上就是本文的全部分享了,如有问题,欢迎指出,如有帮助,点个赞,鼓励一下作者吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/177040.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度学习之基于Pytorch卷积神经网络的图像分类系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介二、功能三、图像分类系统四. 总结 一项目简介 基于PyTorch卷积神经网络的图像分类系统是一种应用深度学习技术来实现图像分类任务的系统。本摘要将对该系统…

Qt QWidget、QDialog、QMainWindow的区别

QWidget QWidget是Qt框架中最基础的窗口类&#xff0c;可以理解为用户界面的最基本单元。QWidget类提供了一个空白窗口&#xff0c;可以通过继承该类来创建自定义的窗口类。QWidget类提供了基本的窗口属性和方法&#xff0c;如大小、位置、标题、图标等。 QDialog QDialog是…

多路IO—POll函数,epoll服务器开发流程

引言 "在计算机网络编程中&#xff0c;多路IO技术是非常常见的一种技术。其中&#xff0c;Poll函数和Epoll函数是最为常用的两种多路IO技术。这两种技术可以帮助服务器端处理多个客户端的并发请求&#xff0c;提高了服务器的性能。本文将介绍Poll和Epoll函数的使用方法&am…

TiDB x 北京银行丨新一代分布式数据库的探索与实践

导读 随着业务规模的扩大&#xff0c;传统数据库面临诸多限制&#xff0c;分布式数据库成为解决之道。本文 介绍了北京银行在数字化转型过程中对分布式数据库技术的探索&#xff0c;分享了 TiDB 在北京银行的应用历程和未来展望 。 本文根据北京银行软件开发中心罗水华先生在…

VS2019 C# mysql数据库使用EF

mysql 安装mysql-8.0.18-winx64 mysql-connector-net-8.0.18.msi mysql数据库.net开发驱动&#xff0c; 要在工程中引入connector安装后目录中的mysql.data.dll;如果直接在nutget中下载mysql.data.dll&#xff0c;那么就不用下载.net开发驱动包 mysql-for-visualstudio-1.…

设计模式_状态模式

状态模式 介绍 设计模式定义案例问题堆积在哪里解决办法状态模式一个对象 状态可以发生改变 不同的状态又有不同的行为逻辑游戏角色 加载不同的技能 每个技能有不同的&#xff1a;攻击逻辑 攻击范围 动作等等1 状态很多 2 每个状态有自己的属性和逻辑每种状态单独写一个类 角色…

Spring底层原理(四)

Spring底层原理(四) 本章内容 模拟实现Spring中的几个常见BeanFactory后置处理器 常见的BeanFactory后置处理器 GenericApplicationContext context new GenericApplicationContext(); context.registerBean("config",Config.class); context.registerBean(Conf…

YOLOv7优化:独家创新(Partial_C_Detect)检测头结构创新,实现涨点 | 检测头新颖创新系列

💡💡💡本文独家改进:独家创新(Partial_C_Detect)检测头结构创新,适合科研创新度十足,强烈推荐 SC_C_Detect | 亲测在多个数据集能够实现大幅涨点 收录: YOLOv7高阶自研专栏介绍: http://t.csdnimg.cn/tYI0c ✨✨✨前沿最新计算机顶会复现 🚀🚀🚀YOLO…

试题二(15分)和试题三(15分) (软件设计师笔记)

&#x1f3e0;个人主页&#xff1a;尘觉主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是尘觉&#xff0c;希望我的文章可以帮助到大家&#xff0c;您的满意是我的动力&#x1f609; 在csdn获奖荣誉: &#x1f3c6;csdn城市之星2名 ⁣⁣⁣⁣ ⁣⁣⁣⁣ ⁣⁣⁣…

模糊C均值聚类(FCM)python

目录 一、模糊C均值聚类的原理 二、不使用skfuzzy的python代码 三、 使用skfuzzy的python代码 一、模糊C均值聚类的原理 二、不使用skfuzzy的python代码 import numpy as np import random import matplotlib.pyplot as plt plt.rcParams[font.sans-serif][SimHei] plt.r…

保障效率与可用,分析Kafka的消费者组与Rebalance机制

系列文章目录 上手第一关&#xff0c;手把手教你安装kafka与可视化工具kafka-eagle Kafka是什么&#xff0c;以及如何使用SpringBoot对接Kafka 架构必备能力——kafka的选型对比及应用场景 Kafka存取原理与实现分析&#xff0c;打破面试难关 防止消息丢失与消息重复——Kafka可…

【WinForm详细教程四】WinForm中的ProgressBar 、ImageList和ListView控件

文章目录 1.ProgressBar2. ImageList3.ListView控件 1.ProgressBar 用于显示某个操作的进度。 属性&#xff1a; Value: 表示当前进度条的值&#xff0c;其范围由Min和Max决定。Step: 设置每次调用PerformStep()方法时增加的步长。MarqueeAnimationSpeed: 在Style设置为Marq…

二叉树问题——前/中/后/层遍历问题(递归与栈)

摘要 博文主要介绍二叉树的前/中/后/层遍历(递归与栈)方法 一、前/中/后/层遍历问题 144. 二叉树的前序遍历 145. 二叉树的后序遍历 94. 二叉树的中序遍历 102. 二叉树的层序遍历 103. 二叉树的锯齿形层序遍历 二、二叉树遍历递归解析 // 前序遍历递归LC144_二叉树的前…

MySQL连接的原理⭐️4种优化连接的手段性能提升240%

MySQL连接的原理⭐️4种优化连接的手段性能提升240%&#x1f680; 前言 上两篇文章我们说到MySQL优化回表的三种方式&#xff1a;索引条件下推ICP、多范围读取MRR与覆盖索引 MySQL的优化利器⭐️索引条件下推&#xff0c;千万数据下性能提升273%&#x1f680; MySQL的优化…

黄金矿工小游戏

欢迎来到程序小院 黄金矿工 玩法&#xff1a;点击开始游戏&#xff0c;黄金和钩子&#xff0c;钩子会左右摆动&#xff0c;对准黄金位置点击鼠标左键钓起黄金加对应时间&#xff0c;钓起黑色四块减去响应时间&#xff0c;快去挖矿吧^^。开始游戏https://www.ormcc.com/play/ga…

主播直播美颜SDK:提升颜值的秘诀

当下&#xff0c;主播们往往依赖于主播直播美颜SDK&#xff0c;这个技术工具为他们提供了一个让自己看起来更好看的机会。本文将深入探讨主播直播美颜SDK的工作原理、应用和影响&#xff0c;揭示提升颜值的秘诀。 一、主播直播美颜SDK是什么&#xff1f; 主播直播美颜SDK是一…

Latex排版SIGGRAPH总结(持续总结中...)

本文学习总结自&#xff1a;How to use the ACM SIGGRAPH / TOG LaTeX template 相关文件&#xff1a;百度网盘 首先解压 “my paper” 中的文件&#xff0c;并用Latex打开mypaper.tex. 多行连等公式 \begin{equation}表示编号公式&#xff0c;\[ \]表示无编号公式 无编号\b…

JMeter:断言之响应断言

一、断言的定义 断言用于验证取样器请求或对应的响应数据是否返回了期望的结果。可以是看成验证测试是否预期的方法。 对于接口测试来说&#xff0c;就是测试Request/Response&#xff0c;断言即可以针对Request进行&#xff0c;也可以针对Response进行。但大部分是对Respons…

精益制造的工具与方法有什么区别?ECRS工时分析软件的功能和价值

精益制造是一套价值创造系统&#xff0c;它强调在生产过程中减少浪费、提高效率和质量&#xff0c;从而实现持续改进和优化。在精益制造的理念下&#xff0c;企业需要运用一系列的工具和方法来提升生产管理水平。这些工具和方法不仅包括传统的精益工具&#xff0c;如5S、持续改…

三.RocketMQ单机安装及集群搭建

RocketMQ单机安装及集群搭建 一&#xff1a;安装环境1.软硬件要求2.下载RocketMQ 二.安装单机MQ1.上传并解压2.目录介绍3.修改MQ启动时初始JVM内存4.启动NameServer与Broker5.测试RocketMQ 三.RocketMQ集群搭建1.集群概念特点2.集群模式分类3.集群工作流程4.双主双从集群搭建4.…