共计 11724 个字符,预计需要花费 30 分钟才能阅读完成。
最开始学 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 的
WebGL
WebGL 使得网页在支持 HTML
2. 基本用法
当没有设置宽度和高度的时候,canvas 会初始化宽度为 300 像素和高度为 150 像素。该元素可以使用 CSS 来定义大小,但在绘制时图像会伸缩以适应它的框架尺寸:如果 CSS 的尺寸与初始画布的比例不一致,它会出现扭曲。
如果绘制出来的图像是扭曲的、实际坐标和指定的坐标不匹配,可以尝试用 width 和 height 属性为
canvas 起初是空白的。为了展示,首先脚本需要找到渲染上下文,然后在它的上面绘制。
<-- canvas 元素 -->
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 作为缩放因子,将会增大单位尺寸变成两个像素。形状的尺寸将会变成原来的两倍。
ctx.scale(9, 3);
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 8, 20);
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 就会一直使用最初的值。当一个状态值被改变时,我们分两种情况考虑。
- 如果使用 beginPath()开始一个新的路径,则不同路径使用不同的值。
- 如果没有使用 beginPath()开始一个新的路径,则后面的值会覆盖前面的值(后来者居上原则)。
Canvas 状态的保存和恢复,主要用于以下三种场合。
- 图形或图片裁切。
- 图形或图片变形。
- 以下属性改变的时候: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 是用于标识要使用的合成或混合模式操作的字符串
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. 使用多层画布去画一个复杂的场景
某些对象需要经常移动或更改,而其他对象则保持相对静态。在这种情况下,可能的优化是使用多个
例如,假设有一个游戏,其 UI 位于顶部,中间是游戏性动作,底部是静态背景。在这种情况下,可以将游戏分成三个
5. 用 CSS 设置大的背景图
如果像大多数游戏那样,你有一张静态的背景图,用一个静态的
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,都保留,新图像在现有的下面绘制
1. 解析
source 和 destination 是在 globalCompositeOperation 属性中经常用到的两个术语,用于描述不同的图像或图像混合模式。下面是这两个术语的具体含义:
- source(源图像)指新绘制的图像,它是将要绘制的图形数据。
- destination(目标图像)指画布上已经存在的旧图像。
图层的顺序会影响最终结果的外观。
- “atop”,将前景图像合成到背景图像上并裁剪多余部分。
- “in”,显示新图层与旧图层相交的部分,并且在新图层之上呈现旧图层
- “out”,显示旧图像与新图像不相交的部分,并且在旧图像之上呈现新图片
- “over”,新添加的内容会出现在其他所有内容之上
不显示的部分,要么被裁剪,要么变透明
先后顺序的问题:
- source,则代表 source 是前景图像
- destination,则代表 destination 是前景图像
over 和 atop 的区别
- over 在 alpha 值为 1.0 的地方会完全覆盖目标图像,而 alpha 值小于 1.0 的地方则会显示出混合结果。
- atop 前景图像的分辨率大于背景图图像之外的部分都会被截断。
事件操作
在 Canvas 中,常见的事件共有三种,即鼠标事件、键盘事件和循环事件。
1. 鼠标事件
在 Canvas 中,鼠标事件分为以下三种。
- 鼠标按下:mousedown
- 鼠标松开:mouseup
- 鼠标移动:mousemove
将鼠标当前的坐标值减去 canvas 元素的偏移位置,则 x、y 为鼠标在 canvas 中的相对坐标
2. 键盘事件
在 Canvas 中,常用的键盘事件有两种。
- 键盘按下:keydown
- 键盘松开: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 中,对于物体的捕获,可以分为以下四种情况来考虑。
- 矩形的捕获。
- 圆的捕获。
- 多边形的捕获。
- 不规则图形的捕获。
多边形以及不规则图形的捕获非常复杂,采用的方法是分离轴定理(SAT)和最小平移向量(MTV)。
1.1 矩形的捕获
如果鼠标点击坐标落在矩形上,则说明捕获了这个矩形;如果鼠标点击坐标没有落在矩形上,则说明没有捕获到这个矩形。
图矩形捕获
// 判断矩形是否被点击
if (mouse.x> rect.x &&
mouse.x < rect.x + rect.width &&
mouse.y > rect.y &&
mouse.y
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