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

news2024/12/1 0:37:12

目录

  • 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.coloradmin.cn/o/1159365.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何制作一款资源网站app

简介 平时生活学习中我们会经常登录各种网站&#xff0c;比如看电影&#xff0c;看视频学习&#xff0c;找资料等等。有时想找到一个靠谱的网站&#xff0c;花了很长时间也找不到。我自己收集了很多好的网站&#xff0c;主要是找资源的&#xff0c;然后我做了一个导航app软件&…

webpack 高级

高级配置就是要进行 webpack 优化&#xff0c;让代码在编译、运行时性能更好 主要从以下角度去优化&#xff1a; 1、提升开发体验 2、提升打包构建速度 3、减少代码体积 4、优化代码运行性能 一、提升体验 1、SourceMap 为什么 打包出来的所有css和js合并成了一个文件&#…

虚拟机部署与发布J2EE项目(Linux版本)

&#x1f3ac; 艳艳耶✌️&#xff1a;个人主页 &#x1f525; 个人专栏 &#xff1a;《Spring与Mybatis集成整合》《Vue.js使用》 ⛺️ 越努力 &#xff0c;越幸运。 1.jdk安装配置 打开虚拟机 Centos 登入账号&#xff0c;并且使用MobaXterm进行连接 1.1. 传入资源 连接…

【算法专题】双指针—盛最多水的容器

一、题目解析 分析这个题目不难得出一个容积公式 二、算法原理 解法一&#xff1a;暴力枚举&#xff08;超时&#xff09; 套用上述的容积公式&#xff0c;使用两个for循环来枚举出所有可能的情况&#xff0c;再挑出最大值即可&#xff0c;但是这种写法会超时&#xff0c;导致…

React使用富文本CKEditor 5,上传图片并可设置大小

上传图片 基础使用&#xff08;标题、粗体、斜体、超链接、缩进段落、有序无序、上传图片&#xff09; 官网查看&#xff1a;https://ckeditor.com/docs/ckeditor5/latest/installation/integrations/react.html 安装依赖 npm install --save ckeditor/ckeditor5-react cked…

【C++类和对象:解锁面向对象编程的奇妙世界】

【本节目标】 1.面向过程和面向对象初步认识 2.类的引入 3.类的定义 4.类的访问限定符及封装 5.类的作用域 6.类的实例化 7.类的对象大小的计算 8.类成员函数的this指针 1.面向过程和面向对象初步认识 C语言是面向过程的&#xff0c;关注的是过程&#xff0c;分析出求…

Nginx搭载负载均衡及前端项目部署

目录 ​编辑 一.Nginx安装 1.安装所需依赖 2.下载并解压Nginx安装包 3.安装nginx 4.启动Nginx服务 二.Tomcat负载均衡 1.准备环境 1.1 准备两个Tomcat 1.2 修改端口号 1.3 配置Nginx服务器集群 2.效果展示 ​编辑三.前端项目打包 ​编辑四.前端项目部署 1.上传项目…

你还不会下载网页上的视频嘛???(超级简单!!)

小伙伴们大家好&#xff0c;废话不多说&#xff0c;直接上干货&#xff1a; F12进入开发者页面 然后F5刷新一下 右键&#xff0c;在新的页面打开 就会看到一个单独的视频网页&#xff0c;右键另存为就好啦

Redis入门02-基础概念

目录 常用的简单操作命令 Redis字符串中的SDS Redis事务 Key的过期时间 Redis实现缓存简单示例 常用的简单操作命令 Redis提供了多种数据类型&#xff0c;包括字符串&#xff08;String&#xff09;、哈希&#xff08;Hash&#xff09;、列表&#xff08;List&#xff09…

Spring系列之基础

目录 Spring概述 Spring的优点 Spring Framework的组成 总结 Spring概述 Spring 是目前主流的 Java Web 开发框架&#xff0c;是 Java 世界最为成功的框架。该框架是一个轻量级的开源框架&#xff0c;具有很高的凝聚力和吸引力。它以Ioc&#xff08;控制反转&#xff09;和…

GAMP源码阅读:RINEX文件读取

原始 Markdown文档、Visio流程图、XMind思维导图见&#xff1a;https://github.com/LiZhengXiao99/Navigation-Learning 文章目录 1、readobsnav()&#xff1a;Rinex 文件读取主入口函数2、readrnxfile()&#xff1a;传入文件路径&#xff0c;读取起止时间内数据4、readrnxfp()…

“原生感”暴涨311%,这届年轻人不再爱浓妆?丨小红书数据分析

近年来&#xff0c;越来越多美妆博主在社交媒体平台安利“原生感妆容”&#xff0c;即我们所熟知的“伪素颜妆”、“裸妆”、“白开水妆”&#xff0c;显然&#xff0c;追求“原生感”成为当代妆容主流。通过小红书数据分析工具&#xff0c;查看#原生感妆容 话题&#xff0c;近…

一位69岁美国老程序员的自述:使用Delphi开发了一款软件仅仅销售了半年赚够钱就退休了

我不确定谁会感兴趣&#xff0c;但你是点击按钮的那个人......所以我就说了。 我是一名 69 岁&#xff08;截至 2008 年&#xff09;的退休程序员&#xff08;译者注&#xff1a;Delphi社区的一位网友说本文主人公已经在前几年去世&#xff0c;但是主人公的网站依然还能访问Del…

Mojo::UserAgent模块做的一个快速爬虫项目

use Mojo::UserAgent;my $ua Mojo::UserAgent->new; my $proxy duoip:8000;# 使用爬虫IP $ua->proxy(http, $proxy) # 设置http爬虫IP->proxy(https, $proxy); # 设置https爬虫IPmy $res $ua->get(音乐网址); if ($res->is_success) {print $res->body; …

让企业的数据用起来,数据中台=数据治理?

加gzh“大数据食铁兽”&#xff0c;了解更多数据治理信息。 先说结论&#xff1a;数据中台是数据管理/治理的工具之一&#xff0c;数据治理是3分技术7分管理及运营。 数据中台的定义&#xff1a; 狭义的数据中台指在企业内部通过对数据半成品、算法、模型、工具等能力的积累&a…

apk反编译修改教程系列---简单去除apk联网权限 其他权限 无法自动更新等【四】

往期教程&#xff1a; apk反编译修改教程系列-----修改apk应用名称 任意修改名称 签名【一】 apk反编译修改教程系列-----任意修改apk版本号 版本名 防止自动更新【二】 apk反编译修改教程系列-----修改apk中的图片 任意更换apk桌面图片【三】 目前基本所有的apk都有联网设…

蚂蚁蚁盾发布实体产业「知识交互建模引擎」,最快10分钟定制AI风控模型

数字化起步晚、数据分散稀疏、专业壁垒高、行业知识依赖「老师傅」&#xff0c;是很多传统产业智能化发展面临的难题。2023年云栖大会上&#xff0c;蚂蚁集团安全科技品牌蚁盾发布“知识交互建模引擎”&#xff0c;将实体产业知识与AI模型有机结合&#xff0c;助力企业最快10分…

【C++】类的默认成员函数----const成员函数(超详细解析)

目录 一、前言 二、const成员函数 &#x1f34e;const修饰类的成员函数 &#x1f4a6;问题1 &#x1f4a6;问题2 &#x1f4a6;针对const成员函数的常考面试题&#xff08;重点&#xff01;&#xff01;&#xff09; &#x1f350;取地址及const取地址操作符重载 三…

可靠的互联网兼职平台,平常可以做副业充实生活

在互联网时代&#xff0c;越来越多的人开始通过网络来寻找兼职副业的机会&#xff0c;能够更灵活地安排自己的时间&#xff0c;实现自己的收入增值。那么找到一个正规可靠的线上兼职平台就是一个比较重要的事情&#xff0c;这里分享几个正规靠谱的线上兼职副业平台&#xff0c;…

生物信息学 | 借助 AI 更高效地开启研究

生物信息学 (Bioinformatics) 是指利用应用数学、信息学、统计学和计算机科学的方法&#xff0c;研究生物学问题。 随着计算机科学技术的发展&#xff0c;AI 在解决复杂又颇具挑战的生物学研究问题方面&#xff0c;显露出极大的优势&#xff0c;进一步加速了传统研究范式的转变…