canvas初探实践-第一篇

添加canvas元素

作为新手,比如我,探索canvas是在页面上加个canvas元素开始的。

<style type="text/css">
  #id {
    border: 1px dashed gray;
  }
</style>
<canvas id="canvas">不支持</canvas>

刷新页面后,页面上就能看到一个虚线框,这个就是默认大小的canvas元素了。 把canvas宽度和高度设置大些,在css里定义好宽度和高度后,事实上并没有效果。解决的办法直接在html属性中定义。

<style type="text/css">
  #id {
    border: 1px dashed gray;
  }
</style>
<canvas id="canvas" width=500 height=500>不支持</canvas>

绘制一条直线

canvas只是一张白纸,而绘制的笔由 document.getElementById('canvas').getContext("2d") 对象提供。

const cxt = document.getElementById('canvas').getContext("2d");

我们从坐标原点作为起点(0,0),离起点x轴100,y轴200处作为终点(100,200),绘制一条直线。 首先告诉画笔移动到起点位置

cxt.moveTo(0, 0);

然后告诉画笔,从起点开始画直线,一直画到终点位置

cxt.lineTo(100, 200);

最后告诉画笔已经确定好点了,可以开始绘制

cxt.stroke();

新手需要了解,canvas的坐标系是 W3C坐标系,y轴正方向向下,坐标原点在屏幕的左上角。

至此,已经学习了三个重要的 API: moveTo, lineTo, stroke。

绘制多边形

思考下:多边形是多条直线按照一定角度连接在一起的,那定义好各个点坐标,分别调用 moveTo、 lineTo。当然还有个更便捷的方式实现:
lineTo方法是可以重复调用的,第一次调用lineTo后,画笔自动移动到终点坐标位置,第二次调用lineTo后,该起点坐标就为上一个终点坐标,以此类推。
绘制直角三角形

const cnv = document.getElementById('canvas');
const cxt = cnv.getContext("2d");

cxt.beginPath();
cxt.moveTo(60, 180);
cxt.lineTo(120, 120);
cxt.lineTo(120, 180);
cxt.closePath();
cxt.stroke();

解析:
首先确定直角三角形的三个坐标点,然后调用beginPath, 开始一条新路径;
接下来画笔移动到其中一个坐标点,从这个坐标点开始一直画直线;
最后调用 closePath, 关闭当前路径,将前面两条线闭合;
最后调用 stroke,绘制直角三角形。

绘制正多边形
多边形是按照一定角度连接的,确定了角度就可以通过三角函数来绘制多边形了,如下:

//正多边形
/**
 n : 表示n边型
 dx、dy:表示n边型中心坐标
 size:表示n边型大小(边的长度)
*/
function createPolygon(ctx, n, dx, dy, size) {
  //每个角的角度
  const degree = (2 * Math.PI)/n;
  cxt.beginPath();
  for(let i=0; i<n; i++) {
    // 三角函数
    let x = Math.cos(i*degree);
    let y = Math.sin(i*degree);
    cxt.lineTo( x * size + dx, y * size + dy );
  }
  cxt.closePath();
}
cxt.strokeStyle="rgba(255,0,0,0.7)";
// 绘制正五多边形
createPolygon(cxt, 5, 100, 100, 60);
cxt.stroke();

提示:canvas只是提供了笔和纸,至于如何绘制各种效果,需要新手复习下数学几何、物理学知识了。

路径

它是canvas中非常重要的概念,除了矩形,其它所有基本图形(包括直线、多边形、圆形、弧线、贝塞尔曲线)都是以路径为基础。

方法 说明
beginPath() 开始一条新的路径
closePath() 闭合当前路径
isPointPath() 判断某一个点是否存在于当前路径

状态

一条路径里,每一次绘制图形(调用stroke()或fill())时,canvas会检测整个程序定义的所有状态,当一个状态值没有被改变,canvas就一直使用最初的值。

cxt.beginPath();
// 设置直线的宽度 10px,canvas中是像素单位(px)
cxt.lineWidth = 10;
// 绘制一条直线,宽度是 10px。 记为 A
cxt.moveTo(60, 180);
cxt.lineTo(120, 120);
cxt.closePath();
cxt.stroke();

// 绘制一条直线,记为 B
cxt.moveTo(100, 110);
cxt.lineTo(200, 210);
cxt.closePath();
cxt.stroke();

// 绘制一条直线,宽度是 20px。记为 C
cxt.lineWidth = 20;
cxt.moveTo(10, 10);
cxt.lineTo(100,100);
cxt.closePath();
cxt.stroke();

// 绘制一条直线,记为 D
cxt.moveTo(50, 30);
cxt.lineTo(300,200);
cxt.closePath();
cxt.stroke();

// 重新开启一条路径,绘制一条直线,记为 E
cxt.beginPath();
cxt.moveTo(90, 90);
cxt.lineTo(00,400);
cxt.closePath();
cxt.stroke();
// 重新开启一条路径,绘制一条直线,记为 F
cxt.beginPath();
cxt.lineWidth = 50;
cxt.moveTo(90, 90);
cxt.lineTo(00,400);
cxt.closePath();
cxt.stroke();

最终效果:线条A、B、C、D、E宽度为 20px,F宽度为50px。
分析:首先我们定义的状态是线条宽度;

  1. 绘制线条E,我们重新开启了一条新路径,调用stroke方法开始绘制,这时候线条宽度并非默认值,而是调用该stroke方法前最后定义的 20px,因为canvas会检测整个程序定义的线条宽度。
  2. 我们首先定义的线条宽度是10px, 但是后面又重新定义20px,覆盖了之前的状态。
  3. 同一条路径中的直线A、B、C、D,虽然每次都调用stroke方法,但是最终生效的宽度是该路径中最后一次调用stroke方法之前检测到的状态。
  4. 不同路径里设置的宽度不会覆盖其它路径里的宽度值,所有路径里F线条虽然设置了50px,但是其它线条设置的宽度值并未受影响。

结论:状态值改变时,分两种情况考虑:

  1. 如果使用 beginPath()开启一条新的路径,则不同路径使用不同的值。
  2. 如果在一条路径内,则后面的值会覆盖前面的值。 当然,lineWidth有点特殊,如果后面设置的值小于前面设置的值,最终效果还是显示前面大的值。

状态操作-保存和恢复

canvas上下文提供了 save 和 restore 这一对API,保存状态和在上下文中恢复保存的状态。
示例:

cxt.lineWidth = 5;
cxt.save();
cxt.beginPath();
// 设置直线的宽度 10px,canvas中是像素单位(px)
cxt.lineWidth = 20;
// 绘制一条直线,宽度是 10px。 记为 A
cxt.moveTo(60, 180);
cxt.lineTo(120, 120);
cxt.closePath();
cxt.stroke();

// 绘制一条直线,记为 B
cxt.moveTo(100, 110);
cxt.lineTo(200, 210);
cxt.closePath();
cxt.restore();
cxt.stroke();

最终效果:直线A的线宽为20px,直线B的线宽为5px。 分析:上面介绍过一个路径里,后面的状态会覆盖前面的状态,上个示例中一条路径里,只能显示最后定义的宽。
本次示例中,由于一开始调用 save方法,保存了 lineWidth=5的状态,绘制直线B时,还原了该状态,最终呈现了不一样的效果。
新手需要注意的是,save和restore一般是成对使用的。
canvas状态的保存和恢复,主要用于以下三种场合:

  1. 图形或图片剪切(clip());
  2. 图形或图片变形;
  3. 其它属性改变的时候:fillStyle、font、globalAlpha、globalCompositeOperation、lineCap、lineJoin、lineWidth、miterLimit、shadowBlur、shadowColor、shadowOffsetX、shadowOffsetY、strokeStyle、textAlign、textBaseline。

变形操作

我们已经探索了canvas的最基本概念和熟悉了canvas的基本操作流程,这次探索变形操作。

方法 说明
translate() 平移
scale() 缩放
rotate() 旋转
transform(), setTransform() 变换矩阵

需要注意的是:translate、scale、rotate这三个方法,都是通过变换矩阵 transform 这个方法来实现的。
transfrom方法和setTransform方法功能类似,但两者间有本质的区别:
每次调用transfrom方法,参考的都是上一次变换后的图形状态,然后再进行变换;
但是setTransform方法会重置图形的状态,然后再进行变换;

首先我们定义一个直角三角形

var cnv = document.getElementById('canvas');
var cxt = cnv.getContext("2d");

//直角三角形
cxt.beginPath();
cxt.lineWidth = 5;
cxt.moveTo(60, 180);
cxt.lineTo(120, 120);
cxt.lineTo(120, 180);
cxt.closePath();

然后将这个三角形做变形操作:

  1. 沿着X轴直线移动100px,Y轴直线移动100px;
  2. 图形在X轴方向缩小0.5倍,Y轴方向放大2倍;
  3. 图形逆时针旋转30°。
//... 省略部分是直角三角形代码
//变形操作
cxt.translate(50, 50);
cxt.scale(0.5, 2);
cxt.rotate(-30 * Math.PI/180);

最后执行绘制

//... 省略部分是直角三角形和变形操作代码
cxt.stroke();

变形操作后的影响:
当平移后,坐标原点移动到(100,100);
当缩放后,直角三角形的左上角坐标,X轴方向缩小一半,Y轴方向放大2倍,图形高度变为原来的两倍长,图形宽度缩小一半,Y轴方向(底)线条高度放大2倍,X轴方向(左右)线条缩小一半;
执行旋转后,图形缩放效果改变,同时按照新的原点坐标逆时针旋转30°。

再次绘制一个矩形图形

//... 省略部分是直角三角形和变形操作代码
cxt.beginPath();
cxt.rect(10,10, 100, 100);
cxt.closePath();
cxt.stroke();

最终效果:矩形也受上面的变形影响。根据之前探索的状态操作,我们可以让矩形正常显示,不受变形影响。

const cnv = document.getElementById('canvas');
const cxt = cnv.getContext("2d");
cxt.save();
//... 省略部分是直角三角形和变形操作代码
cxt.restore();
cxt.beginPath();
cxt.rect(10,10, 100, 100);
cxt.closePath();
cxt.stroke();

最终效果:矩形正常显示,直角三角形依然是变形后效果。
新手注意:如果变形操作后发现接下来的图形跟设想的不一致,可以考虑下save() 和restore()方法了。

总结下:
save方法保存的状态:变形状态(变换矩阵)、绘图状态、剪切状态(clip());
save方法不能保存路径状态,想要一个新的路径,只能调用beginPath();
save方法只能保存状态,不能保存图形,恢复图形只能通过清除画布重绘;

变形实践:图形旋转动画

思路:

  1. 首先图形以canvas的坐标中心为旋转中心,通过translate方法将坐标原点移动到canvas的中心;
  2. 调用rotate方法,旋转指定角度;
  3. 绘制一个矩形,将矩形中心坐标设置为坐标原点,即矩形左上角坐标设置为(-width/2, -height/2);
  4. 调用时间间隔函数比如 requestAnimationFrame,修改旋转角度,不断清除画布重绘,实现图形旋转动画。
const cnv = document.getElementById('canvas');
const cxt = cnv.getContext("2d");
const rectWidth = 100;
const rectHeight = 100;

let i = 0;
function rotate() {
  // 角度累加
  i++;
  // 清除画布
  cxt.clearRect(0,0, cnv.width, cnv.height);
  // 保存当前状态
  cxt.save();
  // 坐标原点移动到画布中心点的100px处
  cxt.translate(rectWidth/2+100, rectHeight/2+100);
  // 指定旋转角度
  cxt.rotate(Math.PI*(i/10));
  // 绘制填充一个蓝色的矩形
  cxt.fillStyle="blue";
  cxt.fillRect(-rectWidth/2, -rectHeight/2, rectWidth, rectHeight);
  // 还原状态,为什么请查看上节介绍
  cxt.restore();
  // 执行动画效果
  requestAnimationFrame(rotate);
  if (i > 360) {
    i = 0;
  }
}
requestAnimationFrame(rotate);

圆形和渐变

绘制圆形
API:
cxt.arc(圆心坐标x, 圆心坐标y, 半径, 开始角度, 结束角度, anticlockwise), anticlockwise为boolen值,true时,表示按逆时针方向绘制,false相反。
例子:绘制一个圆

const cnv = document.getElementById('canvas');
const cxt = cnv.getContext("2d");

function drawBall(x, y, radius, style) {
  cxt.save();
  cxt.beginPath();
  // 圆的开始角度是 0°,结束角度是 360°
  cxt.arc(x, y, radius, 0, 360 * Math.PI/180, false);
  cxt.closePath();
  cxt.fillStyle = style || 'red';
  cxt.fill();
  cxt.restore();
}
// 绘制一个圆,圆心坐标(250,250),半径50px, 填充样式为 红颜色
drawBall(250, 250, 50, 'red');

渐变

  1. 线性渐变 线性渐变,指的是一条直线上进行的渐变。
    API例子示例
let gnt = cxt.createLinearGradient(x1, y1, x2, y2);
gnt.addColorStop(value1, color1);
gnt.addColorStop(value2, color2);
cxt.fillStyle = gnt;
cxt.fill();

解释: 想要实现线性渐变,需要以下三个步骤: 1). 调用 createLinearGradient 方法创建一个 linearGradient 对象,并赋值给变量gnt;
2). 调用 linearGradient 对象(即gnt)的 addColorStop 方法多次,第一次表示渐变开始的颜色,第二次表示渐变结束时的颜色。第三次则以第二次渐变颜色作为开始颜色,进行渐变,以此类推;
3). 把 linearGradient 对象(即gnt)赋值给 fillStyle属性,并且调用fill()方法绘制有渐变色的图形;
参数说明:
x1、y1表示渐变色开始点的坐标,x2、y2表示渐变色结束点的坐标,表示绘制从点(x1, y1) 到点(x2, y2) 的线性渐变。
addColorStop 参数:
value表示渐变位置的偏移量,取值为 0~1之间的任意值,color表示渐变颜色,取值为任意颜色值。

开始点和结束点坐标之间有以下三种关系:
1). 如果y1和y2相同,表示沿着水平方向从左到右渐变,记作 X1->X2;
2). 如果x1和x2相同,表示沿着垂直方向从左到右渐变,记作 y1->y2;
3). 如果x1和x2不同,y1和y2不同,表示渐变色沿着矩形对角线方向渐变,记作 (x1->x2, y1->y2);

代码例子:

横向的线性渐变, X1->X2

// X轴方向 y1 =y2
cxt.save();
var gnt1 = cxt.createLinearGradient(0, 150, 200, 150);
gnt1.addColorStop(0, 'HotPink');
gnt1.addColorStop(1, 'white');
cxt.fillStyle = gnt1;
cxt.fillRect(0,0, 200,200);
cxt.restore();

纵向的线性渐变, y1->y2

// Y轴方向 x1 = x2
cxt.save();
var gnt2 = cxt.createLinearGradient(0, 300, 0, 450);
gnt1.addColorStop(0, 'HotPink');
gnt1.addColorStop(1, 'white');
cxt.fillStyle = gnt2;
cxt.fillRect(0,300, 200,200);
cxt.restore();

对角线的线性渐变,(x1->x2, y1->y2)

cxt.save();
// 矩形对角线方向 x1 != x2, y1 != y2
var gnt3 = cxt.createLinearGradient(300,300, 500, 500);
gnt3.addColorStop(0, 'HotPink');
gnt3.addColorStop(1, 'white');
cxt.fillStyle = gnt3;
cxt.fillRect(300,300, 200,200);
cxt.restore();
  1. 径向渐变 径向渐变,是一种从起点到终点、颜色从内到外进行的圆形渐变(从中间向外拉,像圆一样)。径向渐变是圆形渐变或椭圆形渐变,颜色不再沿着一条直线渐变,而是从一个起点向所有方向渐变。 API例子示例
let gnt = cxt.createRadialGradient(x1, y1, r1, x2, y2, r2);
gnt.addColorStop(value1, color1);
gnt.addColorStop(value2, color2);
cxt.fillStyle = gnt;
cxt.fill();

解释: 想要实现径向渐变,需要以下三个步骤: 1). 调用 createRadialGradient 方法创建一个 radialGradient 对象,并赋值给变量gnt;
2). 调用 radialGradient 对象(即gnt)的 addColorStop 方法多次,第一次表示渐变开始的颜色,第二次表示渐变结束时的颜色。第三次则以第二次渐变颜色作为开始颜色,进行渐变,以此类推;
3). 把 radialGradient 对象(即gnt)赋值给 fillStyle属性,并且调用fill()方法绘制有渐变色的图形; 参数说明:
(x1, y1)表示渐变开始圆心的坐标,r1表示渐变开始圆的半径。
(x2, y2)表示渐变结束圆心的坐标,r2表示渐变结束圆的半径。
调用 createRadialGradient 方法,会从渐变开始的圆心位置(x1, y1)向渐变结束的圆心位置(x2, y2)进行颜色渐变。起点为开始圆心,终点为结束圆心,由起点向终点扩散,直至终点外边框。 addColorStop 参数:
value表示渐变位置的偏移量,取值为 0~1之间的任意值,color表示渐变颜色,取值为任意颜色值。

代码例子:

// 圆形
cxt.save();
cxt.beginPath();
cxt.arc(80,80,50,0, 360 *Math.PI/180, false);
cxt.closePath();
//径向渐变
var gnt4 = cxt.createRadialGradient(100, 60, 10, 80, 80, 50);
gnt4.addColorStop(0,'white');
gnt4.addColorStop(0.9, 'orange');
gnt4.addColorStop(1, 'rgba(0,0,0,0)');   
cxt.fillStyle = gnt4;
cxt.fill();
cxt.restore();

最终效果:一个鸡蛋黄

圆形、渐变、变形实践:圆球绕椭圆做圆周运动

思路:

  1. 首先绘制一个圆,给这个圆添加渐变效果,看起来立体感,具体参考上面的代码;
  2. 绘制一个椭圆轨迹,首先分别计算椭圆的X轴半径、Y轴半径的比率,调用 scale(ratioX, ratioY),进行变形操作,最后按照按照比率确定圆的中心坐标,绘制一个圆;
  3. 根据椭圆标准方程,结合三角函数,计算出椭圆上任意点的坐标;
  4. 调用时间间隔函数比如 requestAnimationFrame,修改旋转角度,不断清除画布重绘,实现圆球绕椭圆做圆周运动动画。
const cnv = document.getElementById('canvas');
const cxt = cnv.getContext("2d");

// 椭圆
function ellipse(cxt, centerX, centerY, radiusX, radiusY) {
  cxt.save();
  let r = (radiusX > radiusY) ? radiusX: radiusY;
  let ratioX = radiusX /r;
  let ratioY = radiusY /r;
  cxt.scale(ratioX, ratioY);
  cxt.beginPath();
  cxt.arc( centerX / ratioX, centerY / ratioY, r, 0, 360 * Math.PI/180, false );
  cxt.closePath();
  cxt.stroke();
  cxt.restore();
}

// 带渐变色的圆球
function drawBall(cxt,x,y,radius) {
  cxt.save();
  cxt.beginPath();
  cxt.arc(x,y,radius,0, 360*Math.PI/180, false);
  cxt.closePath(); 
  let gnt = cxt.createRadialGradient(x,y,10,x,y,50);
  gnt.addColorStop(0,'white');
  gnt.addColorStop(0.9, 'orange');
  gnt.addColorStop(1, 'rgba(0,0,0,0)');  
  cxt.fillStyle=gnt;
  cxt.fill();
  cxt.restore();
}
// 累加旋转角度
let angle = 0;
function drawFrame() {
  requestAnimationFrame(drawFrame);
  cxt.clearRect(0,0, cnv.width, cnv.height);
  // 太阳
  drawBall(cxt, cnv.width/2, cnv.height/2, 30 );
  // 椭圆轨迹
  ellipse(cxt, cnv.width/2, cnv.height/2, 200, 100);
  // 椭圆标准方程
  let circlePointX1 = cnv.width/2 + Math.cos(angle) * 200;
  let circlePointY1 = cnv.height/2 + Math.sin(angle) * 100;
  //绕太阳运行的圆球
  drawBall(cxt,circlePointX1, circlePointY1, 30 );
  angle += 0.02;
  if(angle >= 360) {
    angle = 0;
  }
};
drawFrame();

椭圆任意点的坐标公式推导:
椭圆的标准方程: (x/a)² + (y/b)² = 1, 其中 a为椭圆x轴半径,b为椭圆y轴半径;
因为: (cosA)² + (sinA)² = 1
根据三角函数,得出
x/a = cosA,y/a = sinA
故:任意点坐标为:
x = a * cosA
y = b * sinA
最终公式:

x = centerX + Math.cos(angle) * radiusX;
y = centerY + Math.sin(angle) * radiusY;

新手探索canvas时,最好还是要复习下数学知识。

结束

至此,新手探索旅程本次结束了,这次探索中,接触到了canvas元素的初始化,路径、状态的概念和操作,以及探索了如何操作多线条、矩形、变形、圆形、渐变,动手实践了相关代码,介绍了注意事项和所需的数学知识,可以说canvas的基础大部分基本都已经探索完毕。
下一次旅行我们将探索图形像素操作、阴影效果、文本操作、基本物理动画实现、事件响应实现、以及探索下常用的边界检测和碰撞检测。

@作者:白云飘飘(534591395@qq.com)

@github: https://github.com/534591395 欢迎关注我的微信公众号:

微信公众号
或者微信公众号搜索 新梦想兔,关注我哦。

关注下面的标签,发现更多相似文章
评论
说说你的看法