在学习一项新技术以前,先了解这项技术的历史发展及成因会帮助咱们更深入的理解这项技术。javascript
历史上,canvas最先是由Apple Inc. 提出的,在Mac OS X webkit中建立控制板组件使用,而在canvas称为HTML草案及标准以前,咱们是经过一些替代方式去绘图的,好比为人所诟病的Flash,以及很是强大的SVG(Scalable Vector Graphics,可伸缩的矢量标记图),还有只能在IE(IE 5.0以上的版本)中使用的VML(Vector Markup Language,矢量可标记图)。甚至于有些前端可使用div+css来完成绘图。css
总的来讲,没有canvas的时候,在浏览器绘制图形是比较复杂的,而在canvas出现以后,绘制2D图形相对变得容易了。html
NOTE: 用div绘制一些简单的图形,如矩形,圆形,三角形,梯形,倒也算是没那么复杂。
但canvas也有缺点。由于canvas本质上是一个与 分辨率相关 的 位图画布 ,也就注定了在不一样分辨率下,canvas绘制的内容显示的时候会有所不一样。此外,canvas绘制的内容 不属于任何DOM元素 ,在浏览器的元素查看器中也找不到,那天然没法检测鼠标点击了canvas中的哪一个内容,很显然,这两方面,canvas都是不如SVG的。前端
举个例子:若是使用CSS设置canvas元素的尺寸,那可能会致使绘制出来的图形变得扭曲,如长方形变正方形,圆形变椭圆等,这是由于画布尺寸和元素尺寸是不同的,画布会自动适应元素的尺寸,若是两者是成比例的,那么画布就会等比例缩放,不会出现扭曲。
这么说来,canvas有这么明显的缺点,那直接使用SVG岂不是更好?java
No,听过一句话吗?没有完美的方案,只有适不适合。es6
SVG是基于XML的,那么就说明,SVG里面的元素均可以认为是 DOM元素 ,能够启用DOM操做,同时,SVG中每一个绘制的图像均被视为对象,若SVG对象属性变化,浏览器会自动重现图形。web
以上是SVG的优点,但经过这个优点,咱们也能发现一些问题:算法
回到主题。canvas
canvas是经过JavaScript进行2D图形的绘制,而 <canvas>
标签自己是没有任何绘制能力的,它仅仅是一个容器。在绘制时,canvas是逐像素的进行渲染的,一旦图形绘制完成,该元素就再也不被浏览器所关注(脚本执行结束,绘制的图形也不属于DOM)。浏览器
值得注意的是,在HTML标准(whatwg标准)中明确的指出: Authors should not use the canvas
element in a document when a more suitable element is available. 因此,不要滥用元素。
canvas目前几乎被全部的浏览器支持,可是IE 9.0 以前的版本不支持
canvas
元素
canvas是一个HTML元素,因此要使用canvas,首先须要:
<canvas id="canvas" width="600" height="300"> 当前浏览器不支持canvas </canvas>
在第一行HTML代码中能够看到两个属性:width
和 height
,它指明了画布的宽高,在上文中提到过,不要使用CSS规定尺寸,由于当CSS规定的尺寸和画布尺寸比例不一致时,没法成比例缩放,致使绘制出来的图形变得扭曲。在没有设置画布大小时,canvas默认会初始化成300px * 150px的画布。
“当前浏览器不支持canvas”是元素的内容,但他只是做为一个后备内容(即fallback content),只有当浏览器不支持canvas时,这个内容才会被显示出来。
canvas元素自己没有绘制能力,只是做为一个容器,因此须要经过JavaScript这类脚本进行绘制:
const canvas = document.getElementById('canvas'); const context = canvas.getContext('2d');
上面的HTML+JS代码是使用canvas所必须的,不管要绘制什么内容,这几行代码不可缺乏。
getContext()
是canvas元素提供的方法,用于获取绘制上下文(或者说渲染上下文,The rendering context),他只有一个参数:上下文格式。这里传入2d
表示获取2D图像绘制环境。因为getContext
是canvas元素提供的方法,故咱们能够经过检测getContext
方法的存在性来检查浏览器的支持性。
context变量的类型是CanvasRenderingContext2D
。渲染上下文很差理解,能够理解为画图用的笔刷。
在画布中如何肯定绘制的位置?是坐标。
在canvas中,画布的左上角为原点,横轴为x轴表示宽,纵轴为y轴表示高1。原点的位置是能够移动的,咱们暂时不考虑原点的移动问题。
在w3c school 中,将canvas提供的绘制API大体分为如下几种2:
在上面这个例子中,包含了矩形,圆形,线,文字及“文字”几大块内容,细讲下去,会涉及到很多API,会使得本文变得很长,并且没有必要,值得一提的是贝塞尔曲线,这是二维图形应用程序的数学曲线,通常的矢量图形软件就是经过它来精确画出曲线的,贝塞尔曲线是计算机图形学中至关重要的参数曲线3。
以上图片按顺序分别是一次贝塞尔曲线,二次贝塞尔曲线,三次贝塞尔曲线。从图中,能够很清楚的看到,一次贝塞尔曲线其实是一条直线。固然,还有更高阶次的曲线,不过canvas只提供了二次和三次贝塞尔曲线。
以二次贝塞尔曲线的API为例:
quadraticCurveTo(cp1x, cp1y, x, y);
(cp1x, cp1y)表示控制点坐标,(x, y)表示结束点坐标。这里还缺乏一个起始点坐标,假设是(x0, y0),那这个(x0, y0)是谁?
就是在调用 quadraticCurveTo
函数时,context(绘制上下文)所处的坐标。举个例子:
var cxt = canvas.getContext('2d'); // 认为canvas已经获取到 cxt.beginPath(); cxt.moveTo(120, 90); cxt.quadraticCurveTo(130, 80, 130, 70); cxt.quadraticCurveTo(115, 70, 115, 50); cxt.quadraticCurveTo(115, 30, 155, 30); cxt.quadraticCurveTo(195, 30, 195, 50); cxt.quadraticCurveTo(195, 70, 155, 70); cxt.quadraticCurveTo(135, 90, 120, 90); cxt.stroke();
这段代码运行结果就是一个对话框(在第一张图片中体现),能够看到,在调用二次贝塞尔曲线以前,咱们设置了起点,即,将笔刷移动到坐标(120, 90),在以后调用中,都是之前一次贝塞尔曲线的终点做为本次曲线的起点。
这时候可能会有人问:我去掉这个moveTo
的调用是否是就画不出来了?若是后续是调用lineTo
函数,那还真就画不出来了。可是别忘了,还有一次贝塞尔曲线,这就是条直线,他是以(cp1x, cp1y)为起点,(x,y)为终点的一条直线。因此说,去掉moveTo
后,只会影响到第一条曲线的绘制。可是若是删除最后一行代码stroke()
,那么程序运行结束时,在浏览器上啥都看不到。
由此,咱们应该思考另外一个问题:为何stroke()
函数是必须的呢?
其实,canvas是一种基于状态的绘制,依照此,能够将canvas提供的API分为两种:状态设置,具体绘制。
stroke()
、fill()
等函数就是将内容绘制到canvas画布容器中的函数。
arc()
、lineTo()
、rect()
等函数就是设置笔刷状态的函数。
在那种玄幻类型的电影、电视剧里面就常常能看到某个道士虚空画符,画完以后往前一推,就印在了对应的符或者人身上了。道士虚空画符,这个过程就像是canvas设置笔刷状态的过程。
往前一推,这个就是具体的绘制了,怎么绘制咱不知道,反正这符是画上去了。(前文提到过,canvas是 逐像素渲染 的)
“文字”的绘制,注意,这个文字是打了引号的,普通文字,咱们绘制只须要调用fillText()
便可,而这里所指的文字是点阵字体,在单片机或者LCD这类程序中,经过点亮一系列的点,显示出文字或图案,点亮的过程较为复杂,能够简单的理解为LCD上的像素点置为1时点亮该点,为0时不点亮(实际可能相反)。那么canvas这里的“文字”绘制也是同样的道理,经过创建文字对应的字体库,当须要绘制某个文字的时候,在字体库中找到对应的文字点阵,而后将点阵中标志为1的位置点亮(填充)便可。
实际操做时,可能并非点亮这么简单,你可能会想要制做出更酷的内容,用圆形去填充,用矩形去填充,甚至说想要制做出动态爆炸的效果,这时候就牵扯到一些其余的计算了。
上图是一个用矩形填充的示例,数字对应8x8的点阵。
先思考一个问题,假设如今咱们已经学会了绘制一个圆形的方法,如今要求作出一个和物理学相关的动画:平抛运动。
如今该如何去实现呢?
可能看到这个问题的时候,有些人瞬间懵圈了:我就学了个绘制圆的函数,你就让我模拟这么高难度的动画,你这分明是想谋害郑!
可能也有人会想到,平抛运动,在高中物理学中学到过,基本都只是研究一个小球的问题,在2维平面中,这小球彻底能够视做一个圆,可不就只须要学会画圆就好了?
经此,咱们继续往下思考,在平抛运动中的小球,假设水平方向设有初始速度v0,除了重力外,不受到其余外力影响,也即存在一个重力加速度g(为了计算简单,咱们能够简单的设为g = 10m/s^2
),同时竖直方向没有初速度vh(或称vh = 0;
),以下图:
从图中,咱们能够看到一些颇有意思的现象,如:小球的水平方向恰好和canvas画布的横轴一致,竖直方向也和纵轴方向保持一致。
而后由平抛运动对应的物理公式:
// 竖直方向无初速度,水平方向没有外力 x = v0 * t; // 水平方向位移 h = 1/2 * g * t * t; // 竖直方向位移 // 竖直方向有初速度 h = vh * t - 1/2 * g * t * t; // 竖直方向位移
发现(x, h)和canvas上的坐标(x, y)是一致的,并且咱们也不是在作物理题,也就是说,v0, t, g, vh这些参数都是已知的,咱们惟一须要作的就是,计算出任意时刻的(x, h),也即小球在canvas上的坐标(x, y)。
分析结束,咱们如今能够获得小球在任意时刻的位置坐标,那么咱们也就能够在画布上画出来任意时刻的小球。
针对上面的分析,可能会有人说:你这不对,你这个应该是具备特殊性的吧,小球未必是从左边抛出去的,从右边也能够啊,向上抛也能够。
的确,上面的分析只是取出了其中一个比较特殊的状态来研究,限于篇幅(以及本文主题是canvas而非物理),没有推广到更通常的结论,但其实,这些分析已经足够了,不管是位移仍是速度,他都是矢量,带有方向,那么咱们不妨规定:以canvas的坐标轴,数值增长的方向为正向,那么从右边抛出,能够认为是反向,能够表示为-v0
,最终经过计算位移的公式,能够获得正确的坐标(但这时候算坐标x是比较麻烦的,不能直接使用上述公式)。
分析这么多,说点儿咱最关心的实现。
在以前的分析中,咱们知道想求小球任意时刻所在位置坐标,须要的参数有:v0, t, g, vh。这些参数应该存放在哪里呢?怎么设计这个数据结构?
咱们固然能够直接将这些参数设为全局变量,但这显然是不合适的,这些参数里,惟一适合设为全局变量的是重力加速度g。而v0, t, vh这些都应该是小球自身的“属性”,因此咱们应该将其抽象成一个类。
function Ball(r, v0, vh, t) { this.r = r; this.v0 = v0; this.vh = vh; this.t = t; this.x = 0; this.h = 0; this.calcX = function() { /* 计算水平位移 */ } this.calcH = function() { /* 计算竖直位移 */ } } var ball = { x: 0, h: 0, r: 10, v0: 0, vh: 0, g: 10}; // 重力加速度不管是做为全局变量仍是小球属性,都可 // es6以后 class Ball { constructor(); }
以上三种方式,各有各的好处,选择一个合适的方式便可。
“你这说物理我就头大,有没有更简单的?”
更简单也有啊,反正并无要求100%还原物理学场景:
var ball = { x: 0, y: 0, r: 10, vx: 5, vy: 0, g: 5 }; setInterval(() => { ball.vy += ball.g; // 竖直方向速度增长 ball.y += ball.vy; // 竖直方向位移 ball.x += ball.vx; // 水平方向位移 cxt.clearRect(0, 0, 800, 300); cxt.beginPath(); cxt.fillStyle = 'black'; cxt.arc(ball.x, ball.y, ball.r, 0, 2*Math.PI); cxt.fill(); }, 50);
OK,结束了。
这就是高级一点的动画。可能在学几个函数,这个动画会更炫一点。好比学完矩形填充再掌握一点rgba的知识,你能够作个“尾巴”出来,即长尾效应。具体只须要将上述代码中的cxt.clearRect()
替换成:
cxt.fillStyle = 'rgba(255, 255, 255, 0.2)'; cxt.fillRect(0, 0, 800, 300);
这就能显得我们编码能力很厉害的样子。
作到这一步仍是不知足:小球一个劲儿的向下掉,这动画没一下子就没了。
不要紧,我们能够作“碰撞检测”啊。好像又是一个高大上的词汇,但实际上也没什么高大上的,若是基于本节第一部分的分析,那咱还得考虑一下碰撞形成的动量损失的问题,挺复杂的。
可是简化版就好说了啊。小球碰到上/下边界,竖直方向速度反向,同时速率减半。左右边界能够有相似的处理。
if (ball.r + ball.x > canvas_width) { ball.vx *= -0.5 } if (ball.r + ball.y > canvas_height) { ball.vy *= -0.5; }
NOTE:碰撞检测在这里指的是“ 边界检测”,小球落到边界的时候再继续下落显然是没有意义的,由于后面的动画我们是看不到的。因此要么碰到边界就中止,要么从新开始,或者进行其余处理,总之,不能出现无心义的动画。像之前玩的贪吃蛇,会有各类墙的存在,控制的小蛇在碰到墙的时候,游戏就失败了,或者说没有墙的时候,小蛇会从另外一个方向出来。
说了这么多,你会发现,本文不只没有直接的罗列不一样的DEMO来介绍函数,更是在尽可能避免过多的介绍canvas中的API。
我的看来,canvas其实就是一个函数库,他和咱们平时使用的那些什么forEach,splice,split,map,reduce没什么区别,都是封装好了直接用的,查一查函数手册就能够了解用法了,多用几回就会比较熟悉了。
刚进大学的时候,专业课老师就告诉咱们,程序=算法+数据结构,即便到如今,也有不少人在强调这一点。若是你有心,再回想一下上一节内容,在分析平抛运动的时候,我本质上是在考虑算法问题;在设计小球的类时,考虑了面向对象,但更多的是在考虑数据结构的问题,在考虑了这些内容的基础上,我才开始了具体的实现。
参考资料: