当前位置: 首页 > 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;图像分支中的…...

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇&#xff0c;在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下&#xff1a; 【Note】&#xff1a;如果你已经完成安装等操作&#xff0c;可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作&#xff0c;重…...

变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析

一、变量声明设计&#xff1a;let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性&#xff0c;这种设计体现了语言的核心哲学。以下是深度解析&#xff1a; 1.1 设计理念剖析 安全优先原则&#xff1a;默认不可变强制开发者明确声明意图 let x 5; …...

Linux 文件类型,目录与路径,文件与目录管理

文件类型 后面的字符表示文件类型标志 普通文件&#xff1a;-&#xff08;纯文本文件&#xff0c;二进制文件&#xff0c;数据格式文件&#xff09; 如文本文件、图片、程序文件等。 目录文件&#xff1a;d&#xff08;directory&#xff09; 用来存放其他文件或子目录。 设备…...

ubuntu搭建nfs服务centos挂载访问

在Ubuntu上设置NFS服务器 在Ubuntu上&#xff0c;你可以使用apt包管理器来安装NFS服务器。打开终端并运行&#xff1a; sudo apt update sudo apt install nfs-kernel-server创建共享目录 创建一个目录用于共享&#xff0c;例如/shared&#xff1a; sudo mkdir /shared sud…...

Spring Boot 实现流式响应(兼容 2.7.x)

在实际开发中&#xff0c;我们可能会遇到一些流式数据处理的场景&#xff0c;比如接收来自上游接口的 Server-Sent Events&#xff08;SSE&#xff09; 或 流式 JSON 内容&#xff0c;并将其原样中转给前端页面或客户端。这种情况下&#xff0c;传统的 RestTemplate 缓存机制会…...

通过Wrangler CLI在worker中创建数据库和表

官方使用文档&#xff1a;Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后&#xff0c;会在本地和远程创建数据库&#xff1a; npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库&#xff1a; 现在&#xff0c;您的Cloudfla…...

2021-03-15 iview一些问题

1.iview 在使用tree组件时&#xff0c;发现没有set类的方法&#xff0c;只有get&#xff0c;那么要改变tree值&#xff0c;只能遍历treeData&#xff0c;递归修改treeData的checked&#xff0c;发现无法更改&#xff0c;原因在于check模式下&#xff0c;子元素的勾选状态跟父节…...

智能仓储的未来:自动化、AI与数据分析如何重塑物流中心

当仓库学会“思考”&#xff0c;物流的终极形态正在诞生 想象这样的场景&#xff1a; 凌晨3点&#xff0c;某物流中心灯火通明却空无一人。AGV机器人集群根据实时订单动态规划路径&#xff1b;AI视觉系统在0.1秒内扫描包裹信息&#xff1b;数字孪生平台正模拟次日峰值流量压力…...

稳定币的深度剖析与展望

一、引言 在当今数字化浪潮席卷全球的时代&#xff0c;加密货币作为一种新兴的金融现象&#xff0c;正以前所未有的速度改变着我们对传统货币和金融体系的认知。然而&#xff0c;加密货币市场的高度波动性却成为了其广泛应用和普及的一大障碍。在这样的背景下&#xff0c;稳定…...

Go语言多线程问题

打印零与奇偶数&#xff08;leetcode 1116&#xff09; 方法1&#xff1a;使用互斥锁和条件变量 package mainimport ("fmt""sync" )type ZeroEvenOdd struct {n intzeroMutex sync.MutexevenMutex sync.MutexoddMutex sync.Mutexcurrent int…...