当前位置: 首页 > news >正文

Canvas绘图

Canvas绘图

Canvas的意义

随着前端的不断发展,页面特效越来越炫酷,W3C组织也不断退出新的CSS特性:例如各种渐变,瀑布流布局,各种阴影,但是随着需求越来越花哨,W3C表示:我去你妈的,你自己画去吧。
于是浏览器就暴露出了Canvas API让用户自己实现各种炫酷的效果。

学习过浏览器的渲染过程,我们可以知道其实浏览器的窗口本身就是一个画布,他根据DOM和CSSOM不断得生成绘制指令来重绘页面。
Canvas其实就是浏览器将绘制指令封装成API给用户进行调用,这也是为什么Canvas的性能要比直接操作DOM的性能更高的原因。

这就是Canvas存在的意义,可以自定义炫酷的效果,可以有比DOM操作更好的性能。

图形绘制API

坐标系

讲绘图之前先讲解一下坐标系。
Canvas的坐标系与浏览器的坐标系相同,都是以左上角为原点,向右为x轴,向下为y轴。

上下文

想用Canvas进行绘制,首先需要拿到Canvas的上下文,它就相当于一个画笔,可以发出各种绘制指令。

<canvas id="canvas" />const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext('2d');

这个 ctx就是canvas的上下文,这个上下文共有4种类型:

  • 2d: 绘制2d图形
  • bitmaprenderer: 绘制位图
  • webgl: 绘制3d图形,只在实现WebGL1的浏览器种可用
  • webgl2: 绘制3d图形,只在实现WebGL2的浏览器种可用

这里只学习最基本的2d图形绘制。

绘制指令

canvas支持4种图形的绘制

  • 直线和矩形
  • 曲线和椭圆
  • 文本
  • 图片

不管绘制哪种图形,都是按照下面的步骤发出绘图指令,和AI还有PS的逻辑很相似:

  1. 开启路径
  2. 设置着色(描边和填充)
  3. 设置路径
  4. 闭合路径
  5. 绘制
ctx.beginPath();  // 开启路径
ctx.strokeStyle = '#aaaaaa'; // 设置描边颜色
ctx.fillStyle = '#111111';   // 设置填充颜色// ... 若干绘图指令ctx.closePath();    // 闭合路径,可选的,也可以不闭合,绘制一条开放的路径,如果使用填充指令来绘制的话,路径会自动闭合。ctx.fill();    // 填充
ctx.stroke();  // 描边
直线绘制

绘制一个三角形演示一下

ctx.moveTo(100, 100); // 移动画笔到100, 100的位置
ctx.lineTo(100, 200); // 从当前位置向100, 200画一条路径
ctx.lineTo(200, 200); 
ctx.closePath();      // 闭合路径形成完整的三角形
ctx.stroke();         // 根据路径描边
矩形绘制

矩形的绘制有三种API

  • 绘制矩形的路径
  • 绘制带描边的矩形
  • 绘制带填充的矩形

三种API不做演示,剩余的圆形,曲线等API也不做演示,可自行查阅文档。
Canvas 教程 - Web API 接口参考 | MDN (mozilla.org)

案例

利用图形API可以制作一个粒子连线效果。
首先定义一个类,用于表示粒子点:

    class Point {constructor() {this.r = 8;this.x = getRandomInt(canvas.width - this.r, this.r);this.y = getRandomInt(canvas.height - this.r, this.r);}draw() {ctx.beginPath();ctx.fillStyle = 'rgba(11, 11, 11, 255)';ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);ctx.closePath();ctx.fill();}}

然后再定义一个类,用于表示粒子图

    class Graph {constructor(count = 30, maxDis = 200) {this.maxDis = maxDis;this.points = new Array(30).fill(null).map(item => new Point());}draw() {for(let i = 0; i < this.points.length; i++) {const p = this.points[i];p.draw();for(let j = i+1; j < this.points.length; j++) {const p2 = this.points[j];const d = Math.sqrt((p.x - p2.x) ** 2 + (p.y - p2.y) ** 2);ctx.beginPath();ctx.moveTo(p.x, p.y);ctx.lineTo(p2.x, p2.y);ctx.closePath();ctx.strokeStyle = `rgba(11, 11, 11, ${255 * (this.maxDis - d) / this.maxDis})`;ctx.stroke();}}}}function getRandomInt(max, min = 0) {return Math.floor( Math.random() * (max - min + 1) ) + min;}

加上最后的代码,就可以实现一副一直抽搐的粒子图。

    function main() {ctx.fillStyle = '#fff';ctx.fillRect(0, 0, canvas.width, canvas.height);new Graph().draw();requestAnimationFrame(main);}main();

清晰度问题

随着页面的放大,Canvas绘制的图形会变得模糊。

要解决这个问题,首先要知道,其实Canvas绘制的结果,就是一张图片。

图像有两种尺寸,一种是自然尺寸,一种是样式尺寸。
自然尺寸就是图像原本的大小,样式尺寸是通过css或者JS设置的尺寸。

图片随着放大会变得模糊,是因为它的样式尺寸大于他的自然尺寸,使得原来的一个像素点需要两个或者更多像素点来显示,但是图片本身并没有包含这么多信息,因此就会变得模糊。

但是Canvas的尺寸是可以自己设置的,绘制的图形大小也是可以自己设置的,也就是自然尺寸是可以更改的。所以利用这个特性,我们只要满足 自然尺寸 = 样式尺寸 * 缩放倍率,就可以做到Canvas不会模糊。

使用下面的代码可以在页面上绘制一个圆

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');function init() {ctx.width = 200;ctx.height = 200;
}init();ctx.beginPath();
const r = 80;
ctx.arc(canvas.width / 2, canvas.height / 2, r, 0, Math.PI * 2);
ctx.strokeStyle = '#fff';
ctx.lineWidth = 10;  // 设置canvas线宽
ctx.stroke();

通过这一段代码可以绘制初始化一个200 * 200的canvas页面,并在canvas的中央绘制了一个半径为80的圆。

现在的代码,当浏览器的缩放倍率变化时,圆会变得模糊。
我们可以修改代码,使得 自然尺寸 = 样式尺寸 * 缩放倍率这个等式永远成立,只要这个等式成立,图像一定是清晰的。

缩放倍率变化后,可以修改Canvas的自然尺寸使得图像依然清晰。

// 修改原始尺寸
function init() {// devicePixelRatio 可以获取当前浏览器的放大倍率canvas.width = 200 * devicePixelRatio; // 样式尺寸 * 缩放倍率 = 原始尺寸canvas.height = 200 * devicePixelRatio;
}

如果想让canvas上绘制的内容也跟着放大和缩小,只需要让绘制指令中的数值也跟着放大和缩小。

ctx.beginPath();
const r = 80 * devicePixelRatio;
ctx.arc(canvas.width / 2, canvas.height / 2, r, 0, Math.PI * 2);
ctx.strokeStyle = '#fff';
ctx.lineWidth = 10 * devicePixelRatio;  // 设置canvas线宽
ctx.stroke();

动画

使用Canvas来做动画,原理很简单,就是每间隔一段时间重新绘制Canvas。
因为JS的定时器不准确,计时间隔小的时候可能会出现掉帧的现象,所以使用 requestAnimationFrame()更合适。

有点类似递归,这样写就可以让浏览器每次刷新时都重新绘制Canvas。

function draw() {// 若干绘制指令reuqestAnimationFrame(draw);
}

文字绘制–代码雨效果

canvas绘制文字的方式很简单。只有下面的几个API

// 设置字体和对齐方式
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.font = fontSize + 'px Verdana';// 填充或者描边文字
ctx.fillText(text, x, y);
ctx.strokeText(text, x, y);

有了这几个API,我们可以制作一个代码雨效果,下面是实现的代码。

	// 文字绘制函数function draw() {// 逐行绘制文字,绘制每行文字之前先绘制一层浅浅的遮罩来降低原有文字的不透明度// 每次绘制文字之前ctx.fillStyle = '#ffffff20';ctx.fillRect(0, 0, canvas.width, canvas.height);ctx.fillStyle = 'green';for(let i = 0; i < col; i++) {// 随机获取一个字符,并绘制,每列绘制一个// rows数组存储的是要绘制的文字的y坐标,有多少列就有多少项ctx.fillText(getRandomChar(), i * fontSize, rows[i]);// 当文字的y坐标同时满足两项时才将文字绘制的y坐标清0if(rows[i] > canvas.height && Math.random() > 0.99) {rows[i] = 0;} else {rows[i] += fontSize;}}}// 使用定时器频繁绘制,这里不使用requestAnimationFrame的原因是帧率太高。setInterval(() => {draw()}, 50);draw();// 辅助函数,获取一个随机字符function getRandomChar() {const chars = '0123456789qwertyuiopasdfghjklzxcvbnm'.split('');const index = Math.floor( Math.random() * chars.length );return chars[index];}

图片绘制–魔棒效果

图像相关的CanvasAPI如下:

ctx.drawImage(img, x, y);              // 在canvas中绘制图像
ctx.getImageData(x, y, width, height); // 获取Canvas的像素信息
ctx.putImageData(imageData, x, y);     // 根据像素信息绘制Canvas
imageData.data.set(greenColor, i);     // 设置Canvas的像素信息

canvas不仅可以将图片绘制出来,甚至还可以拿到像素点的信息。

function init() {const img = new Image();img.src = '';// 加载完成时间,当图像加载完成后执行img.onload((e) => {// 设置canvas尺寸与图片一致canvas.width = img.width;canvas.height = img.height;ctx.drawImage(img, 0, 0); // 在Canvas中绘制图片})
}canvas.addEventListener('click', (e) => {// 获取用户点击的位置在Canvas中的坐标;const x = e.offsetX;const y = e.offsetY;// 取出Canvas的像素信息,这是一个大数组,每四项为一组,分别代表一个像素点的RGBA值const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
})

如果我们想做到点击后修改图片中的颜色,那我们需要先定义一些辅助函数,下面的两个辅助函数能帮助我们更方便的查找Canvas中的颜色。

// 坐标转下标
function point2Index() {return (y * canvas.width + x) * 4;
}// 根据坐标获取像素点信息
function getColor(x, y, imageData) {const i = point2Index(x, y);return [imageData[i],imageData[i+1],imageData[i+2],imageData[i+3]]
}

如果我希望我点击的位置变为绿色,那我们可以修改对应位置的RGBA值,然后将新的像素信息交给Canvas去绘制。

canvas.addEventListener('click', (e) => {// 获取用户点击的位置在Canvas中的坐标;const x = e.offsetX;const y = e.offsetY;// 取出Canvas的像素信息,这是一个大数组,每四项为一组,分别代表一个像素点的RGBA值const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);const clickColor = getColor(x, y, imageData);const greenColor = [0, 255, 0, 255]function _changeColor(x, y) {const i = point2Index(x, y);// 设置ImageData中的值imageData.data.set(greenColor, i);}_changeColor(x, y);// 上面修改的只是内存中的像素点信息,通过putImageData将数组应用到Canvas中。ctx.putImageData(imageData, 0, 0);
})// 因为需要多次获取Canvas中的像素信息,浏览器发出了警告,建议我们加上一个配置
const ctx = canvas.getContext('2d', {willReadFrequently: true // 告诉浏览器将会频繁的读取像素信息,需要做相应优化
})

需求还可以进一步提升,例如我需要像PS的魔棒一样,点击后,相邻相似的颜色都会改变。

我们只需要改写一下 _changeColor()函数,递归得调用它即可。

function _changeColor() {// 如果超出边界,停止递归if(x<0 || x>canvas.width || y<0 || y>canvas.height) {return;}const color = getColor(x, y, imageData);// 如果颜色差值较大,则停止递归if(diffColor(color, clickColor) > 100) {return;}// 已经被改为绿色,不再更改if(diffColor(color, greenColor) === 0) {return;}const i = point2Index(x, y);// 设置ImageData中的值imageData.data.set(greenColor, i);// 递归调用,如果图片比较大,递归可能会栈溢出,可以改用循环来写。_changeColor(x + 1, y);_changeColor(x - 1, y);_changeColor(x, y + 1);_changeColor(x, y - 1);
}// 辅助函数,返回两个函数的颜色差值
function diffColor(color1, color2) {return Math.abs(color1[0] - color2[0]) + Math.abs(color1[1] - color2[1]) + Math.abs(color1[2] - color2[2]) + Math.abs(color1[3] - color2[3])
}

绘制和拖拽

有了前面的Canvas基础,可以来做两个综合案例。
下面制作一个使用画笔图板和一个图形画板,Canvas可以做的效果很多,主要是制作的思路。

画笔画板

图形画板

相关文章:

Canvas绘图

Canvas绘图 Canvas的意义 随着前端的不断发展&#xff0c;页面特效越来越炫酷&#xff0c;W3C组织也不断退出新的CSS特性&#xff1a;例如各种渐变&#xff0c;瀑布流布局&#xff0c;各种阴影&#xff0c;但是随着需求越来越花哨&#xff0c;W3C表示&#xff1a;我去你妈的&…...

逻辑回归评分卡

文章目录 一、基础知识点(1)逻辑回归表达式(2)sigmoid函数的导数损失函数(Cross-entropy, 交叉熵损失函数)交叉熵求导准确率计算评估指标 二、导入库和数据集导入库读取数据 三、分析与训练四、模型评价ROC曲线KS值再做特征筛选生成报告 五、行为评分卡模型表现总结 一、基础知…...

DPDK系列之三十三DPDK并行机制的底层支持

一、背景介绍 在前面介绍了DPDK中的上层对并行的支持&#xff0c;特别是对多核的支持。但是&#xff0c;大家都知道&#xff0c;再怎么好的设计和架构&#xff0c;再优秀的编码&#xff0c;最终都要落到硬件和固件对整个上层应用的支持。单纯的硬件好处理&#xff0c;一个核不…...

LVGL_基础控件滚轮roller

LVGL_基础控件滚轮roller 1、创建滚轮roller控件 /* 创建一个 lv_roller 部件(对象) */ lv_obj_t * roller lv_roller_create(lv_scr_act()); // 创建一个 lv_roller 部件(对象),他的父对象是活动屏幕对象// 将部件(对象)添加到组&#xff0c;如果设置了默认组&#xff0c…...

王道考研操作系统——文件管理

磁盘的基础知识 .txt用记事本这个应用程序打开&#xff0c;文件最重要的属性就是文件名了 保护信息&#xff1a;操作系统对系统当中的各个用户进行了分组&#xff0c;不同分组的用户对文件的操作权限是不一样的 文件的逻辑结构就是文件内部的数据/记录应该被怎么组织起来&…...

商业智能系统的主要功能包括数据仓库、数据ETL、数据统计输出、分析功能

ETL服务内容包含&#xff1a; 数据迁移数据合并数据同步数据交换数据联邦数据仓库...

基于帝国主义竞争优化的BP神经网络(分类应用) - 附代码

基于帝国主义竞争优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码 文章目录 基于帝国主义竞争优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码1.鸢尾花iris数据介绍2.数据集整理3.帝国主义竞争优化BP神经网络3.1 BP神经网络参数设置3.2 帝国主义竞争算…...

将python项目部署在一台服务器上

将python项目部署在一台服务器上 1.服务器2.部署方法2.1 手动部署2.2 容器化技术部署2.3 服务器less技术部署 1.服务器 服务器一般为&#xff1a;物理服务器和云服务器。 我的是物理服务器&#xff1a;这是将服务器硬件直接放置在您自己的数据中心或机房的传统方法。这种方法需…...

【C语言】善于利用指针(二)

&#x1f497;个人主页&#x1f497; ⭐个人专栏——C语言初步学习⭐ &#x1f4ab;点击关注&#x1f929;一起学习C语言&#x1f4af;&#x1f4ab; ​ 目录 导读&#xff1a;1. 字符指针1.1 字符串的引用方式1.2 有趣的面试题 2. 数组指针2.1 一维数组指针的定义2.2 一维数组…...

Python调用C++

https://www.cnblogs.com/renfanzi/p/10276997.html Linux使用Python调用C/C接口(一) - 代码先锋网 linux系统上使用Python调用C生成的.so动态链接库opencv_linux 下python 编译为so ,给c使用_比赛学习者的博客-CSDN博客 https://www.cnblogs.com/shuimuqingyang/p/13618105…...

自己实现扫描全盘文件的函数。

1.自己实现扫描全盘的函数 def scan_disk(dir): global count,dir_count if os.path.isdir(dir): files os.listdir(dir) for file in files: print(file) dir_count 1 if os.path.isdir(dir os.sep file): …...

JSON文件读写

1、依赖文件 #include <QFile> #include <QJsonDocument> #include <QJsonObject> #include <QDebug> #include <QStringList>2、头文件 bool ReadJsonFile(const QString& filePath""); bool WriteJsonFile(const QString&…...

VisualStudio2022环境下Release模式编译dll无法使用TLS函数问题

Debug x86环境下正常使用TLS回调函数 切换到Release发现程序没有使用tls 到C/C > 优化中将全程序优化关闭即可...

ChatGPT基础使用总结

文章目录 一、ChatGPT基础概念大型语言模型LLMs---一种能够以类似人类语言的方式“说话”的软件ChatGPT定义---OpenAI 研发的一款聊天机器人程序&#xff08;2022年GPT-3.5&#xff0c;属于大型语言模型&#xff09;ChatGPT4.0---OpenAI推出了GPT系列的最新模型ChatGPT典型使用…...

解决报错: require is not defined in ES module scope

用node启动mjs文件报错&#xff1a;require is not defined in ES module scope 现象如下&#xff1a; 原因&#xff1a; 文件后缀是mjs, 被识别为es模块&#xff0c;但是node默认是commonjs格式&#xff0c;不支持也不能识别es模块。 解决办法&#xff1a;把文件后缀从.mjs改…...

STM32 10个工程篇:1.IAP远程升级(六)

在IAP远程升级的最后一篇博客里&#xff0c;笔者想概括性地梳理总结IAP程序设计中值得注意的问题&#xff0c;诚然市面上或者工作后存在不同版本的IAP下位机和上位机软件&#xff0c;也存在不同定义的报文格式&#xff0c;甚至对于相似的知识点不同教程又有着完全不同的解读&am…...

【智能家居项目】裸机版本——字体子系统 | 显示子系统

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《智能家居项目》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 今天实现上图整个项目系统中的字体子系统和显示子系统。 目录 &#x1f004;设计思路&#x1…...

PDF中跳转到参考文献后,如何回到原文

在PDF中&#xff0c;点击了参考文献的超链接可以直接跳至参考文献的位置。 如果想从当前参考文献在回到正文中对应位置时&#xff0c;可以通过 Alt \red{\text{Alt}} Alt ← \red{\leftarrow} ← 实现。...

了解基于Elasticsearch 的站内搜索,及其替代方案

对于一家公司而言&#xff0c;数据量越来越多&#xff0c;如果快速去查找这些信息是一个很难的问题&#xff0c;在计算机领域有一个专门的领域IR&#xff08;Information Retrival&#xff09;研究如何获取信息&#xff0c;做信息检索。在国内的如百度这样的搜索引擎也属于这个…...

【多模态融合】TransFusion学习笔记(2)

接上篇【多模态融合】TransFusion学习笔记(1)。 从TransFusion-L到TransFusion ok,终于可以给出论文中那个完整的框架图了&#xff0c;我第一眼看到这个图有几个疑问: Q&#xff1a;Image Guidance这条虚线引出的Query Initialization是什么意思? Q&#xff1a;图像分支中的…...

基于FPGA的PID算法学习———实现PID比例控制算法

基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容&#xff1a;参考网站&#xff1a; PID算法控制 PID即&#xff1a;Proportional&#xff08;比例&#xff09;、Integral&#xff08;积分&…...

工业安全零事故的智能守护者:一体化AI智能安防平台

前言&#xff1a; 通过AI视觉技术&#xff0c;为船厂提供全面的安全监控解决方案&#xff0c;涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面&#xff0c;能够实现对应负责人反馈机制&#xff0c;并最终实现数据的统计报表。提升船厂…...

模型参数、模型存储精度、参数与显存

模型参数量衡量单位 M&#xff1a;百万&#xff08;Million&#xff09; B&#xff1a;十亿&#xff08;Billion&#xff09; 1 B 1000 M 1B 1000M 1B1000M 参数存储精度 模型参数是固定的&#xff0c;但是一个参数所表示多少字节不一定&#xff0c;需要看这个参数以什么…...

Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)

概述 在 Swift 开发语言中&#xff0c;各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过&#xff0c;在涉及到多个子类派生于基类进行多态模拟的场景下&#xff0c;…...

css3笔记 (1) 自用

outline: none 用于移除元素获得焦点时默认的轮廓线 broder:0 用于移除边框 font-size&#xff1a;0 用于设置字体不显示 list-style: none 消除<li> 标签默认样式 margin: xx auto 版心居中 width:100% 通栏 vertical-align 作用于行内元素 / 表格单元格&#xff…...

ios苹果系统,js 滑动屏幕、锚定无效

现象&#xff1a;window.addEventListener监听touch无效&#xff0c;划不动屏幕&#xff0c;但是代码逻辑都有执行到。 scrollIntoView也无效。 原因&#xff1a;这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作&#xff0c;从而会影响…...

零基础在实践中学习网络安全-皮卡丘靶场(第九期-Unsafe Fileupload模块)(yakit方式)

本期内容并不是很难&#xff0c;相信大家会学的很愉快&#xff0c;当然对于有后端基础的朋友来说&#xff0c;本期内容更加容易了解&#xff0c;当然没有基础的也别担心&#xff0c;本期内容会详细解释有关内容 本期用到的软件&#xff1a;yakit&#xff08;因为经过之前好多期…...

关键领域软件测试的突围之路:如何破解安全与效率的平衡难题

在数字化浪潮席卷全球的今天&#xff0c;软件系统已成为国家关键领域的核心战斗力。不同于普通商业软件&#xff0c;这些承载着国家安全使命的软件系统面临着前所未有的质量挑战——如何在确保绝对安全的前提下&#xff0c;实现高效测试与快速迭代&#xff1f;这一命题正考验着…...

回溯算法学习

一、电话号码的字母组合 import java.util.ArrayList; import java.util.List;import javax.management.loading.PrivateClassLoader;public class letterCombinations {private static final String[] KEYPAD {"", //0"", //1"abc", //2"…...

NPOI Excel用OLE对象的形式插入文件附件以及插入图片

static void Main(string[] args) {XlsWithObjData();Console.WriteLine("输出完成"); }static void XlsWithObjData() {// 创建工作簿和单元格,只有HSSFWorkbook,XSSFWorkbook不可以HSSFWorkbook workbook new HSSFWorkbook();HSSFSheet sheet (HSSFSheet)workboo…...