zl程序教程

您现在的位置是:首页 >  IT要闻

当前栏目

前端canvas基础复习,canvas学习笔记,持续记录

2023-03-07 09:44:18 时间

最开始学html5的时候,曾特意了解过canvas,还记得当时为了搞明白canvas的api,绞尽脑汁了很多个日日夜夜。

但实际工作后用的非常少,到现在canvas的api忘的也差不多了。目前打算好好学一下canvas,尝试一下更多的可能性。

相关知识

一些资料的收集:

参考了很多文章,真正需要使用canvas开发的大都侧重于游戏开发,以及基于web平台的一些工具(类似蓝湖、BoradMix这些);前端的范围和广度说大不大、说小不小,Canvas或许能带来更多的可能性。

时至今日,前端能做的早就不是简单的画页面了,WebGL、WebRTC、WebAssembly等等这些技术含量更高的方向,或许我们可以尝试一二。

Canvas基础

1.介绍

Canvas API(画布)是在HTML5中新增的标签用于在网页实时生成图像,并且可以操作图像内容,基本上它是一个可以用JavaScript操作的位图(bitmap)。

Canvas API 提供了一个通过JavaScript 和 HTML的<canvas>元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。

WebGL

Canvas API 主要聚焦于 2D 图形。而同样使用<canvas>元素的 WebGL API 则用于绘制硬件加速的 2D 和 3D 图形。

WebGL 使得网页在支持 HTML <canvas>标签的浏览器中,不需要使用任何插件,便可以使用基于 OpenGL ES 2.0 的 API 在 canvas 中进行 3D 渲染。

2.基本用法

<canvas> 标签只有两个属性 width和height。这些都是可选的,并且同样利用 DOM properties 来设置。

当没有设置宽度和高度的时候,canvas 会初始化宽度为 300 像素和高度为 150 像素。该元素可以使用CSS来定义大小,但在绘制时图像会伸缩以适应它的框架尺寸:如果 CSS 的尺寸与初始画布的比例不一致,它会出现扭曲。

如果绘制出来的图像是扭曲的,尝试用 width 和 height 属性为<canvas>明确规定宽高,而不是使用 CSS。

canvas 起初是空白的。为了展示,首先脚本需要找到渲染上下文,然后在它的上面绘制。<canvas> 元素有一个叫做 getContext() 的方法,这个方法是用来获得渲染上下文和它的绘画功能。getContext()接受一个参数,即上下文的类型。

<-- canvas元素 -->
<canvas id="canvas"></canvas>

<script>
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');  //CanvasRenderingContext2D对象
</script>

CanvasRenderingContext2D对象:https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D

3.检查canvas是否被支持

var canvas = document.getElementById('tutorial');

if (canvas.getContext){
  var ctx = canvas.getContext('2d');
  // drawing code here
} else {
  // canvas-unsupported code here
}

样式和颜色

1.fillStyle

CanvasRenderingContext2D.fillStyle 是 Canvas 2D API 使用内部方式(填充图形)描述颜色和样式的属性。默认值是 #000 (黑色)。

ctx.fillStyle = color;  //字符串颜色代码,符合 CSS3 颜色值标准 的有效字符串

/* 比如 */
ctx.fillStyle = "orange";
ctx.fillStyle = "#FFA500";
ctx.fillStyle = "rgb(255,165,0)";
ctx.fillStyle = "rgba(255,165,0,1)";

ctx.fillStyle = gradient;  //CanvasGradient 对象(线性渐变或者放射性渐变).

/* 比如 */
var lingrad = ctx.createLinearGradient(0,50,0,95);
lingrad.addColorStop(0.5, '#000');
lingrad.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = pattern;

ctx.fillStyle = pattern; //CanvasPattern 对象(可重复图像)。

/* 比如 */
var img = new Image();
img.src = 'https://mdn.mozillademos.org/files/222/Canvas_createpattern.png';
img.onload = function() {
    // 创建图案
    var ptrn = ctx.createPattern(img, 'repeat');
    ctx.fillStyle = ptrn;
}

2.strokeStyle 

CanvasRenderingContext2D.strokeStyle 是 Canvas 2D API 描述画笔(绘制图形)颜色或者样式的属性。默认值是 #000 (black)。

/* 同上 */
ctx.strokeStyle = color;
ctx.strokeStyle = gradient;
ctx.strokeStyle = pattern;

3.渐变 Gradients

经过测试,渐变色未填满整体图形时,最外层颜色会扩散到整个图形的剩余部分;

未填满时

3.1 createLinearGradient

CanvasRenderingContext2D.createLinearGradient()方法用于创建一个沿参数坐标指定的直线的渐变,该方法返回一个线性 CanvasGradient对象。

//创建一个线性渐变色
CanvasGradient ctx.createLinearGradient(x0, y0, x1, y1); 

let gradient=context.createLinearGradient(200,50,400,50);
// 添加一个由偏移(offset)和颜色(color)定义的断点到渐变中。
// 正如函数名:到达指定位置,颜色停止
gradient.addColorStop(0, 'green');
gradient.addColorStop(.5, 'cyan');
gradient.addColorStop(1, 'green');

3.2 createRadialGradient

CanvasRenderingContext2D.createRadialGradient() 是 Canvas 2D API 根据参数确定两个圆的坐标,绘制放射性渐变的方法。

经过测试,开始圆比结束圆大的时候向内渐变,比结束圆小的时候向外渐变。

/* 
* 从100,100,位置开始画一个半径为100的圆
* 向100,100,位置半径半径为10的圆,开始渐变色
* white从外层圆向内,渐变色到达内部圆圆边时停止
* 内部圆会被外层颜色自动扩散从而被填充。
* 可以理解为这个渐变圆和fill填充的图形的重叠部分,为最终图形
*/
var gradient = ctx.createRadialGradient(100,100,100,100,100,10);
gradient.addColorStop(0,"white");
gradient.addColorStop(1,"green");
ctx.fillStyle = gradient;
ctx.fillRect(0,0,200,200);

上方代码的效果

3.3 总结

直线渐变在垂直、倾斜时会左右平移填充整个图片,水平时上下平移。圆形的渐变则是取重叠部分,形成最终的图形。

渐变色填充

canvas栅格

canvas 元素默认被网格所覆盖。通常来说网格中的一个单元相当于 canvas 元素中的一像素。栅格的起点为左上角(坐标为(0,0))。所有元素的位置都相对于原点定位。

栅格

canvas状态属性

在 Canvas 中,如果以下状态属性发生改变的时候,我们可以在这些状态改变之前使用 save()方法来保持,然后在状态保存之后使用 restore()方法恢复。是否需要进行保存和恢复,这个取决于我们的开发需求。

  • 填充效果:fillStyle。
  • 描边效果:strokeStyle。
  • 线条效果:lineCap、lineJoin、lineWidth、miterLimit。
  • 文本效果:font、textAlign、textBaseline。
  • 阴影效果:shadowBlur、shadowColor、shadowOffsetX、shadowOffsetY。
  • 全局属性:globalAlpha、globalCompositeOperation。

填充、描边、剪切

不带fill、stroke的方法都只会在画布上产生路径状态,不会绘制实际图像。调用fill、stroke等等方法之后才会进行绘制。

1.填充(fill)

fill() 是 Canvas 2D API 根据当前的填充样式,填充当前或已存在的路径的方法。采取非零环绕或者奇偶环绕规则。

ctx.rect(10, 10, 100, 100);
ctx.fill();

//填充正方形
ctx.fillRect();
//填充文本
ctx.fillText();

2.描边(stroke)

stroke() 是 Canvas 2D API 使用非零环绕规则,根据当前的画线样式,绘制当前或已经存在的路径的方法。

ctx.rect(10, 10, 100, 100);
ctx.stroke();
//绘制正方形 
ctx.strokeRect(); 
//绘制文本 
ctx.strokeText();

3.裁剪(clip)

clip() 是 Canvas 2D API 将当前创建的路径设置为当前剪切路径的方法。 clip用于设置一个剪切区域,当使用 clip()方法指定剪切区域后,后面所有绘制的图形如果超出这个剪切区域,则超出部分不会显示。

// 创建一个弧形剪切区域
ctx.arc(100, 100, 75, 0, Math.PI*2, false);
ctx.clip();

ctx.fillRect(0, 0, 100,100);

裁剪

常用操作

平移、旋转、放大、缩放等操作不会对已绘制的图像产生任何影响,因为它们修改的是坐标系,之后对之后重新绘制的图像产生影响(相当于用修改后的上下文状态进行绘制)!

1.平移(translate)

translate() 方法,将 canvas 按原始 x 点的水平方向、原始的 y 点垂直方向进行平移变换

ctx.translate(50, 50);
ctx.fillRect(0,0,100,100);

// reset current transformation matrix to the identity matrix
ctx.setTransform(1, 0, 0, 1, 0, 0);

平移

2.旋转(rotate)

(2π = 360)rotate() 方法用于旋转坐标系;

ctx.rotate(45 * Math.PI / 180);
ctx.fillRect(70,0,100,30);

// 这次旋转是一上次旋转45度之后进行旋转,相当于旋转了90度
ctx.rotate(45 * Math.PI / 180);
// reset current transformation matrix to the identity matrix
ctx.setTransform(1, 0, 0, 1, 0, 0);

旋转

3.放大、缩小(scale)

scale() 是 Canvas 2D API 根据 x 水平方向和 y 垂直方向,为 canvas 单位添加缩放变换的方法。

默认的,在 canvas 中一个单位实际上就是一个像素。例如,如果我们将 0.5 作为缩放因子,最终的单位会变成 0.5 像素,并且形状的尺寸会变成原来的一半。相似的方式,我们将 2.0 作为缩放因子,将会增大单位尺寸变成两个像素。形状的尺寸将会变成原来的两倍。

// Scaled rectangle
ctx.scale(9, 3);
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 8, 20);

// Reset current transformation matrix to the identity matrix
ctx.setTransform(1, 0, 0, 1, 0, 0);

同样的可以通过scale实现水平、垂直翻转

ctx.scale(-1, 1);  //水平翻转上下文
ctx.scale(1, -1);  //垂直翻转上下文

scale的副作用

scale()方法会改变图形的左上角坐标、宽度或高度、线条宽度。知道这些,可以让我们更加深入地了解 scale()方法的本质以及避免出现一些低级的 bug。

4.擦除(clearRect)

clearRect()通过把像素设置为透明以达到擦除一个矩形区域的目的。

// 清除一部分画布
ctx.clearRect(10, 10, 120, 100);

//清除整个画布
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);

Transform(矩阵变形)

transform() 是 Canvas 2D API 使用矩阵多次叠加当前变换的方法,矩阵由方法的参数进行描述。

setTransform()和 transform()方法非常相似,都可以对图形进行平移、缩放、旋转等操作,不过两者也有着本质的区别:即每次调用 transform()方法,参考的都是上一次变换后的图形状态,然后再进行变换。但是 setTransform()方法不一样,setTransform()方法会重置图形的状态,然后再进行变换。

// tansform是基于上一个状态进行改变
transform(a (水平缩放,垂直倾斜,水平倾斜,垂直缩放,水平移动,垂直移动);

//setTransform会先重置,再设置矩阵
setTransform(a (水平缩放,垂直倾斜,水平倾斜,垂直缩放,水平移动,垂直移动);

//getTransform() 方法获取当前被应用到上下文的转换矩阵,返回一个 DOMMatrix 对象

坐标点位置判断

1.isPointInStroke()

isPointInStroke()是 Canvas 2D API 用于检测某点是否在路径的描边线上的方法。

2.isPointInPath()

isPointInPath()是 Canvas 2D API 用于判断在当前路径中是否包含检测点的方法。

只支持路径,不支持fillRect、drawImage这些操作

状态保存和恢复

Canvas 是基于「状态」来绘制图形的。每一次绘制(stroke()或 fill()),Canvas 会检测整个程序定义的所有状态,这些状态包括 strokeStyle、fillStyle、lineWidth 等。当一个状态值没有被改变时,Canvas 就会一直使用最初的值。当一个状态值被改变时,我们分两种情况考虑。

  1. 如果使用 beginPath()开始一个新的路径,则不同路径使用不同的值。
  2. 如果没有使用 beginPath()开始一个新的路径,则后面的值会覆盖前面的值(后来者居上原则)。

Canvas 状态的保存和恢复,主要用于以下三种场合。

  1. 图形或图片裁切。
  2. 图形或图片变形。
  3. 以下属性改变的时候:fillStyle、font、globalAlpha、globalCompositeOperation、lineCap、lineJoin、lineWidth、miterLimit、shadowBlur、shadowColor、shadowOffsetX、shadowOffsetY、strokeStyle、textAlign、textBaseline。
ctx.save(); // 保存默认的状态

ctx.fillStyle = "green";
ctx.fillRect(10, 10, 100, 100);

ctx.restore(); // 还原到上次保存的默认状态
ctx.fillRect(150, 75, 100, 100);

图片绘制

1.图形或图片剪切

在 Canvas 中,可以在图形或者图片剪切(clip())之前使用 save()方法来保持当前状态,然后在剪切(clip())之后使用 restore()方法恢复之前保存的状态。

var cnv = $$("canvas");
var cxt = cnv.getContext("2d");

//save()保存状态
cxt.save();
//使用 clip()方法指定一个圆形的剪切区域 
cxt.beginPath();
cxt.arc(70, 70, 50, 0, 360 * Math.PI / 180, true);
cxt.closePath();
cxt.stroke();
cxt.clip();
//绘制一张图片 
var image = new Image();
image.src = 「images/princess.png」;
image.onload = function () {
     cxt.drawImage(image, 10, 20);
}
$$(「btn」).onclick = function () {
	 //restore()恢复状态
	 cxt.restore();
	 //清空画布 
	 cxt.clearRect(0, 0, cnv.width, cnv.height);
	 //绘制一张新图片 
	 var image = new Image();
	 image.src = 「images/Judy.png」;
	 image.onload = function () {
		 cxt.drawImage(image, 10, 20);
	 }
}

2.图像绘制 

//普通
drawImage(image,x,y);
//缩放
drawImage(image,x,y,width,height);
// 切片,图像指定一部分到画布指定位置
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

组合 Compositing

globalCompositeOperation 属性设置要在绘制新形状时应用的合成操作的类型,其中 type 是用于标识要使用的合成或混合模式操作的字符串

https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation

canvas 的优化 

相关文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas

1.在离屏 canvas 上预渲染相似的图形或重复的对象

myEntity.offscreenCanvas = document.createElement("canvas");
myEntity.offscreenCanvas.width = myEntity.width;
myEntity.offscreenCanvas.height = myEntity.height;
myEntity.offscreenContext = myEntity.offscreenCanvas.getContext("2d");

myEntity.render(myEntity.offscreenContext);

2.避免浮点数的坐标点,用整数取而代之

当画一个没有整数坐标点的对象时会发生子像素渲染。浏览器为了达到抗锯齿的效果会做额外的运算。为了避免这种情况,请保证在调用drawImage()函数时,用Math.floor()函数对所有的坐标点取整。

3.不要在用drawImage时缩放图像

在离屏 canvas 中缓存图片的不同尺寸,而不要用drawImage()去缩放它们。

4.使用多层画布去画一个复杂的场景

某些对象需要经常移动或更改,而其他对象则保持相对静态。在这种情况下,可能的优化是使用多个<canvas>元素对您的项目进行分层。

例如,假设有一个游戏,其 UI 位于顶部,中间是游戏性动作,底部是静态背景。在这种情况下,可以将游戏分成三个<canvas>层。UI 将仅在用户输入时发生变化,游戏层随每个新框架发生变化,并且背景通常保持不变。

5.用 CSS 设置大的背景图

如果像大多数游戏那样,你有一张静态的背景图,用一个静态的<div>元素,结合background 特性,以及将它置于画布元素之后。这么做可以避免在每一帧在画布上绘制大图。

6.用 CSS transforms 特性缩放画布

CSS transforms 使用 GPU,因此速度更快。最好的情况是不直接缩放画布,或者具有较小的画布并按比例放大,而不是较大的画布并按比例缩小。

6.关闭透明度

//如果不需要透明度可以关闭透明度
var ctx = canvas.getContext('2d', { alpha: false });

globalCompositeOperation  

  • source-over,现有画布之上绘制图像
  • destination-over,现有画布的下面绘制图形
  • source-in,与现有画布重叠的地方绘制图形,其他地方透明(如单词的意思在source源的内部绘制)
  • source-out,与现有画布不重叠的地方绘制图形,其他地方透明(如单词的意思在source源的外部绘制)
  • source-atop,与现有画布内容重叠的地方绘制,其他地方不透明
  • destination-in,现有内容保留在重叠位置
  • destination-out,现有内容保留不重叠位置
  • destination-atop,都保留,新图像在现有的下面绘制

事件操作

在 Canvas 中,常见的事件共有三种,即鼠标事件、键盘事件和循环事件。

1.鼠标事件

在 Canvas 中,鼠标事件分为以下三种。

  1. 鼠标按下:mousedown
  2. 鼠标松开:mouseup
  3. 鼠标移动:mousemove

将鼠标当前的坐标值减去 canvas 元素的偏移位置,则 x、y 为鼠标在 canvas 中的相对坐标

2.键盘事件

在 Canvas 中,常用的键盘事件有两种。

  1. 键盘按下:keydown
  2. 键盘松开:keyup

3.循环事件

说起如何实现 Canvas 动画,大多数人想到的都是先使用 setInterval()来定时清空画布、然后重绘图形,从而达到动画的效果。事实上,这种方式不能准确地控制动画的帧率,这是因为 setInterval()本身存在一定的性能问题。

在 Canvas 中,一般使用 requestAnimationFrame()方法来实现循环,从而达到动画效果。虽然 requestAnimationFrame 这个名字很长,但其实把它分开来看就很清楚它的含义了:request animation frame,也就是「请求动画帧」的意思。

 //动画循环
(function frame() {
	 window.requestAnimationFrame(frame);
	 //每次动画循环都先清空画布,再重绘新的图形
	 cxt.clearRect(0, 0, cnv.width, cnv.height);

	 //绘制圆 
	 cxt.beginPath();
	 cxt.arc(x, 70, 20, 0, 360 * Math.PI / 180, true);
	 cxt.closePath();
	 cxt.fillStyle = 「#6699FF」;
	 cxt.fill();

	 //变量递增 
	 x += 2;
})();

Canvas 动画的原理

使用 requestAnimationFrame()方法不断地清除 Canvas,然后重绘图形。

物理动画

物理动画,简单来说,就是模拟现实世界的一种动画效果。在物理动画中,物体会遵循牛顿运动定律,如射击游戏中打出去的炮弹会随着重力而降落。

  • 三角函数
  • 匀速运动
  • 加速运动
  • 重力
  • 摩擦力

用户交互

所谓的用户交互,指的是用户可以借助鼠标或键盘参与到 Canvas 动画中去,来实现一些互动的效果。用户交互,往往是借助两个事件来实现的,一个是键盘事件,另外一个是鼠标事件。

1.捕获物体

想要拖曳一个物体或者抛掷一个物体,首先要知道怎么来捕获一个物体。只有捕获了一个物体,才可以对该物体进行相应的操作。

在 Canvas 中,对于物体的捕获,可以分为以下四种情况来考虑。

  1. 矩形的捕获。
  2. 圆的捕获。
  3. 多边形的捕获。
  4. 不规则图形的捕获。

多边形以及不规则图形的捕获非常复杂,采用的方法是分离轴定理(SAT)和最小平移向量(MTV)。

1.1矩形的捕获

如果鼠标点击坐标落在矩形上,则说明捕获了这个矩形;如果鼠标点击坐标没有落在矩形上,则说明没有捕获到这个矩形。

图矩形捕获

//判断矩形是否被点击
if (mouse.x > rect.x &&
  mouse.x < rect.x + rect.width &&
  mouse.y > rect.y &&
  mouse.y < rect.y + rect.height) {
   ……
}

1.2圆的捕获

在 Canvas 中,对于圆来说,可以采用一种高精度的方法来捕获:判定鼠标与圆心之间的距离。如果距离小于圆的半径,说明鼠标落在了圆上面;如果距离大于或等于圆的半径,说明鼠标落在了圆的外面。

dx = mouse.x - ball.x;
dy = mouse.y - ball.y;
distance = Math.sqrt(dx*dx + dy*dy);
if(distance < ball.radius){
   ……
}

2.拖拽物体

在 Canvas 中,如果我们想要拖曳一个物体,一般情况下需要以下三个步骤。

  • 捕获物体:在鼠标按下(mousedown)时,判断鼠标坐标是否落在物体上面,如果落在,就添加两个事件:mousemove 和 moveup。
  • 移动物体:在鼠标移动(mousemove)中,更新物体坐标为鼠标坐标。
  • 松开物体:在鼠标松开(mouseup)时,移除 mouseup 事件(自身事件也得移除)和 mousemove 事件。
cnv.addEventListener("mousedown", function () {
   document.addEventListener("mousemove", onMouseMove, false);
   document.addEventListener("mouseup", onMouseUp, false);
}, false);

三角函数

回过头来才发现高中那会的才是最聪明的时候?

最近在学习canvas的路上越走越黑,canvas充分的自由度带来了无限的可能性。简单的平移、旋转、缩放还可以理解,复杂的变化没点数学基础确实啃不动?‍♂️,三角函数还有点印象,但是记得也不是很清楚了,矩阵已经没印象了....

三角函数:https://baike.baidu.com/item/%E4%B8%89%E8%A7%92%E5%87%BD%E6%95%B0/1652457

矩阵:https://baike.baidu.com/item/%E7%9F%A9%E9%98%B5/18069

反三角函数:https://baike.baidu.com/item/%E5%8F%8D%E4%B8%89%E8%A7%92%E5%87%BD%E6%95%B0/7004029

只记得三角函数,知道角度和一条边求另一条边。反三角?知道边求角?

JS中的运用:https://www.jianshu.com/p/7c6a4f3021a1

Math 类的 sin(x)、cos(x)、tan(x) 中的 x 参数是弧度(弧度 = 角度 × π / 180)

三角函数

  • 正弦(sin) sinA = a / c ,sinθ = y / r
  • 余弦(cos) cosA = b / c ,cosθ = y / r
  • 正切(tan) tanA = a / b ,tanθ = y / x
  • 余切(cot) cotA = b / a, cotθ = x / y

反三角函数

var asin30 = Math.round(Math.asin(sin30) * 180 / Math.PI)
console.log(asin30); //30

var acos60 = Math.round(Math.acos(cos60) * 180 / Math.PI)
console.log(acos60); //60

var atan45 = Math.round(Math.atan(tan45) * 180 / Math.PI)
console.log(atan45); //4

向量与矩阵

相关知识:https://www.modb.pro/db/418935

相关书籍:https://www.zhihu.com/pub/reader/119565272/chapter/975240522307125248