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的逻辑很相似:
- 开启路径
- 设置着色(描边和填充)
- 设置路径
- 闭合路径
- 绘制
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的意义 随着前端的不断发展,页面特效越来越炫酷,W3C组织也不断退出新的CSS特性:例如各种渐变,瀑布流布局,各种阴影,但是随着需求越来越花哨,W3C表示:我去你妈的&…...

逻辑回归评分卡
文章目录 一、基础知识点(1)逻辑回归表达式(2)sigmoid函数的导数损失函数(Cross-entropy, 交叉熵损失函数)交叉熵求导准确率计算评估指标 二、导入库和数据集导入库读取数据 三、分析与训练四、模型评价ROC曲线KS值再做特征筛选生成报告 五、行为评分卡模型表现总结 一、基础知…...
DPDK系列之三十三DPDK并行机制的底层支持
一、背景介绍 在前面介绍了DPDK中的上层对并行的支持,特别是对多核的支持。但是,大家都知道,再怎么好的设计和架构,再优秀的编码,最终都要落到硬件和固件对整个上层应用的支持。单纯的硬件好处理,一个核不…...

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

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

商业智能系统的主要功能包括数据仓库、数据ETL、数据统计输出、分析功能
ETL服务内容包含: 数据迁移数据合并数据同步数据交换数据联邦数据仓库...

基于帝国主义竞争优化的BP神经网络(分类应用) - 附代码
基于帝国主义竞争优化的BP神经网络(分类应用) - 附代码 文章目录 基于帝国主义竞争优化的BP神经网络(分类应用) - 附代码1.鸢尾花iris数据介绍2.数据集整理3.帝国主义竞争优化BP神经网络3.1 BP神经网络参数设置3.2 帝国主义竞争算…...
将python项目部署在一台服务器上
将python项目部署在一台服务器上 1.服务器2.部署方法2.1 手动部署2.2 容器化技术部署2.3 服务器less技术部署 1.服务器 服务器一般为:物理服务器和云服务器。 我的是物理服务器:这是将服务器硬件直接放置在您自己的数据中心或机房的传统方法。这种方法需…...

【C语言】善于利用指针(二)
💗个人主页💗 ⭐个人专栏——C语言初步学习⭐ 💫点击关注🤩一起学习C语言💯💫 目录 导读: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 研发的一款聊天机器人程序(2022年GPT-3.5,属于大型语言模型)ChatGPT4.0---OpenAI推出了GPT系列的最新模型ChatGPT典型使用…...

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

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

【智能家居项目】裸机版本——字体子系统 | 显示子系统
🐱作者:一只大喵咪1201 🐱专栏:《智能家居项目》 🔥格言:你只管努力,剩下的交给时间! 今天实现上图整个项目系统中的字体子系统和显示子系统。 目录 🀄设计思路…...
PDF中跳转到参考文献后,如何回到原文
在PDF中,点击了参考文献的超链接可以直接跳至参考文献的位置。 如果想从当前参考文献在回到正文中对应位置时,可以通过 Alt \red{\text{Alt}} Alt ← \red{\leftarrow} ← 实现。...

了解基于Elasticsearch 的站内搜索,及其替代方案
对于一家公司而言,数据量越来越多,如果快速去查找这些信息是一个很难的问题,在计算机领域有一个专门的领域IR(Information Retrival)研究如何获取信息,做信息检索。在国内的如百度这样的搜索引擎也属于这个…...

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

【网络安全产品大调研系列】2. 体验漏洞扫描
前言 2023 年漏洞扫描服务市场规模预计为 3.06(十亿美元)。漏洞扫描服务市场行业预计将从 2024 年的 3.48(十亿美元)增长到 2032 年的 9.54(十亿美元)。预测期内漏洞扫描服务市场 CAGR(增长率&…...

【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例
文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...

WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成
厌倦手动写WordPress文章?AI自动生成,效率提升10倍! 支持多语言、自动配图、定时发布,让内容创作更轻松! AI内容生成 → 不想每天写文章?AI一键生成高质量内容!多语言支持 → 跨境电商必备&am…...

Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...

全志A40i android7.1 调试信息打印串口由uart0改为uart3
一,概述 1. 目的 将调试信息打印串口由uart0改为uart3。 2. 版本信息 Uboot版本:2014.07; Kernel版本:Linux-3.10; 二,Uboot 1. sys_config.fex改动 使能uart3(TX:PH00 RX:PH01),并让boo…...

嵌入式学习笔记DAY33(网络编程——TCP)
一、网络架构 C/S (client/server 客户端/服务器):由客户端和服务器端两个部分组成。客户端通常是用户使用的应用程序,负责提供用户界面和交互逻辑 ,接收用户输入,向服务器发送请求,并展示服务…...

Python Ovito统计金刚石结构数量
大家好,我是小马老师。 本文介绍python ovito方法统计金刚石结构的方法。 Ovito Identify diamond structure命令可以识别和统计金刚石结构,但是无法直接输出结构的变化情况。 本文使用python调用ovito包的方法,可以持续统计各步的金刚石结构,具体代码如下: from ovito…...

宇树科技,改名了!
提到国内具身智能和机器人领域的代表企业,那宇树科技(Unitree)必须名列其榜。 最近,宇树科技的一项新变动消息在业界引发了不少关注和讨论,即: 宇树向其合作伙伴发布了一封公司名称变更函称,因…...
「全栈技术解析」推客小程序系统开发:从架构设计到裂变增长的完整解决方案
在移动互联网营销竞争白热化的当下,推客小程序系统凭借其裂变传播、精准营销等特性,成为企业抢占市场的利器。本文将深度解析推客小程序系统开发的核心技术与实现路径,助力开发者打造具有市场竞争力的营销工具。 一、系统核心功能架构&…...
在树莓派上添加音频输入设备的几种方法
在树莓派上添加音频输入设备可以通过以下步骤完成,具体方法取决于设备类型(如USB麦克风、3.5mm接口麦克风或HDMI音频输入)。以下是详细指南: 1. 连接音频输入设备 USB麦克风/声卡:直接插入树莓派的USB接口。3.5mm麦克…...