「Canvas」入门到实战,掌握开发思路。

652 阅读14分钟

我们在学一个新东西的时候往往只接触到最基础的东西,大部分入门就真的只是入门,在没有实际运用经验的前提下很难联想到一些实际运用案例的实现思路,感觉学了还是和没学差不多。那本文不仅会给大家介绍一些Canvas的基础api,以及一些基本图形的绘制,还会给大家展示一些Canvas实际的应用案例,介绍其大致的实现思路,带领大家了解canvas的同时,能够对它的应用的实现有一些思路,当你的领悟有了,境界还会远吗。

一、什么是Canvas

MDN: 是 HTML5 新增的元素,可用于通过使用 JavaScript 中的脚本来绘制图形。例如,它可以用于绘制图形、制作照片、创建动画,甚至可以进行实时视频处理或渲染。

二、Canva应用场景

  1. 可视化图表。例如:Echarts
  2. 各种炫(wò)酷(艹)的场景动画。
  3. 场景活动页(刮刮乐、转盘抽奖、红包雨)
  4. h5小游戏(五子棋、俄罗斯方块)
  5. 图形编辑器(web版在线Ps)

三、入门

(1)设置画布

// html
<canvas id="canvas"></canvas>

初始化的画布有个默认300x150的大小,我们也可以手动设置画布的大小,设置画布有三种方式:

  • HTML 设置 width、height;
  • CSS 设置 width、height;
  • JS 动态设置 width、height。

我们一种种方式来看下:

// html方式
<canvas width="400" height="200"></canvas>

// css方式
#canvas{
    width:400px;
    height:200px
}

// js方式
var canvas = document.getElementById("canvas");
canvas.width = 400;
canvas.height = 200;

看起来是不是很简单?那可不是嘛,要不然坑怎么来的?

注意:这三种方式中,HTML和JS的方式是真实的改变了画布的大小,而css只是改变了canvas元素的大小,而实际上画布依旧是300x150的,可以理解为将300x150的内容展示在400x200的容器中,这样就会导致图形变形。

???理解不了?

蒙娜丽莎的微笑逐渐严肃!

(2)点线面

以上我们完成了利用Canvas进行骚操作的最最基本的前提-设置画布。接下来介绍一些关于图形的基础知识-点线面以及如何利用canvas进行绘制点线面。

  • 点:在几何学上,点只有位置,而在形态学中,点还具有大小、形状、色彩、肌理等造型元素。

  • 线:线是点运动的轨迹,又是面运动的起点

  • 面:扩大的点形成了面,一根封闭的线造成了面

基础概念介绍完了,好(zou)好(zou)掌(xing)握(shi)哦~

让我们开始实际操作吧...

绘制-点

为了方便学习,这里我们将点就当作是一个圆(躲避杠精)。

想要在canvas上绘制图形,首先我们需要通过HTMLCanvasElement.getContext()先拿到canvas元素的上下文,通过这个上下文对canvas进行绘制,canvas作为画布,你可以将这个上下文理解为画笔🖌️。

 var canvas = document.getElementById("canvas"); // 获取元素引用
 var context = canvas.getContext("2d"); // 获取canvas的上下文

这心操的~

<script>
    var canvas = document.getElementById("canvas");
    var context = canvas.getContext("2d");
    var cx = canvas.width = 400;
    var cy = canvas.height = 200;
    
    context.beginPath(); // 开始一条路径或重置当前路径
    context.arc(100, 100, 20, 0, Math.PI * 2, true); // 画圆
    context.closePath(); // 创建从当前点回到起始点的路径(关闭路径)
    context.fillStyle = 'rgb(255,255,255)'; // 填充样式
    context.fill(); // 开始绘制或填充
</script>

来看看效果:

这个点有点大!,担心各位看官姥爷耗太多心力去找点,我刻意将它画的大了一点。

这里我们主要用到了arc()方法,该方法用于创建弧/曲线(创建圆或部分圆)。

context.arc(x,y,r,sAngle,eAngle,counterclockwise);

  • x: 圆的中心的 x 坐标
  • y: 圆的中心的 y 坐标
  • r: 圆的半径。
  • sAngle: 起始角,以弧度计。(弧的圆形的三点钟位置是 0 度)。
  • eAngle: 结束角,以弧度计。
  • counterclockwise: 可选。规定应该逆时针还是顺时针绘图。False = 顺时针,true = 逆时针。

比如我们绘制半圆可以这样用

context.arc(100, 100, 20, 0, Math.PI * 1.0, false); // so easy~ 

效果如图

上面我们提到过arc()这个方法还可以用来画弧,那要怎么画呢?请看代码

context.beginPath();
context.arc(100,75,50,0,1.5*Math.PI); 
context.strokeStyle="red" // 设置描边颜色为红色
context.stroke(); // 进行路线

效果如图

心细的小伙伴到这里可能已经发现了一些端倪了~

我们绘制弧线除了改变角度之外,主要将fillStylefill() 替换成了 strokeStylestroke(),

简单理解就是 fill 是填充, 而 stroke 是描边。

到这里是不是感觉“哦~”,仔细回忆一下发现我们是不是还少了一句代码

context.closePath()

那让我们加上来看一下是什么效果:

context.beginPath();
context.arc(100,75,50,0,1.5*Math.PI); 
context.closePath(); // 关闭路径
context.strokeStyle="red" // 设置描边颜色为红色
context.stroke(); // 进行路线

这样是不是就是明白了closePath()到底干啥事了吧,对于不是填充的图形来说是否加closePath()就会有以上区别。而对于填充的图形来说,有无closePath()都不重要,填充的都是闭合图形,就像ps里面的填充颜色一样,它填充的是一个闭合的区域,你不加closePath()它也会默认闭合,不信你看!

context.beginPath();
context.arc(100,75,50,0,1.5*Math.PI); 
context.fillStyle="red" // 设置描边颜色为红色
context.fill(); // 进行路线

喝杯水咱再来看看画直线!

绘制-直线

刚才我们介绍过线是点的运动轨迹,那这条线我们得画多少点呀(不会只有我一个人有这么"严谨"的逻辑思维吧)?别这么deng!

正所谓两点一线。我们要绘制一条直线,首先要确定两个点,绘制之前先介绍两个方法

context.moveTo(x,y); // 设置起始点

context.lineTo(x,y); // 设置第下一个点

  • x:路径的目标位置的 x 坐标
  • y:路径的目标位置的 y 坐标

闲话少说,直接上代码:

context.beginPath(); // 开始一条路径
context.moveTo(50,50); // 绘制直线的起点位置
context.lineTo(100,100); // 添加一个新点,然后在画布中创建从上一个点到该点的线条
context.stroke();

效果如图

注意:没有设置moveTo(),则第一个lineTo()将充当moveTo的作用。例如:

context.beginPath();
context.lineTo(20,20);
context.lineTo(60,60);
context.lineTo(120,80);
context.lineTo(50,100);
context.stroke()

效果如图

绘制线条就是将每个点依次连起来,

绘制-矩形

context.beginPath();
context.fillStyle = '#fff'; 
context.fillRect(10, 10, 100, 100); // 绘制实心矩形
context.strokeStyle = '#fff'; // 线条
context.strokeRect(130, 10, 100, 100); // 绘制空心矩形

效果如图

context.fillRect(x,y,width,height);

context.strokeRect(x,y,width,height);

  • x: 矩形左上角的 x 坐标
  • y: 矩形左上角的 y 坐标
  • width: 矩形的宽度,以像素计
  • height: 矩形的高度,以像素计

讲到这里顺便介绍一下关于矩形的另一个方法,用于清空给定矩形内的指定像素。

context.clearRect(x,y,width,height)

来看看它的实际效果:

ctx.fillStyle="#000";
ctx.fillRect(0,0,300,150); // 绘制一个宽300,高150的矩形
ctx.clearRect(20,20,100,50); // 清除一个 100x50 的区域

效果如图

记住这个方法,这家伙有大用~!

讲到这里,我们绘制的一些基础知识大致介绍完了。大家对于canvas的也有了一定的认识,绘制思路是不是也有了一点想法。接下来我们介绍一些让我们绘制的图形更加的“美观”的属性和方法。由于篇幅关系,我先将一些常用的属性和方法列出来,然后在一个综合的例子里给大家示例一下。

颜色、样式和阴影

属性描述
fillStyle设置或返回用于填充绘画的颜色、渐变或模式
strokeStyle设置或返回用于笔触的颜色、渐变或模式
shadowColor设置或返回用于阴影的颜色
shadowBlur设置或返回用于阴影的模糊级别
shadowOffsetX设置或返回阴影距形状的水平距离
shadowOffsetY设置或返回阴影距形状的垂直距离

方法描述
createLinearGradient()创建线性渐变(用在画布内容上)
createPattern()在指定的方向上重复指定的元素
createRadialGradient()创建放射状/环形的渐变(用在画布内容上)
addColorStop()规定渐变对象中的颜色和停止位置

线条样式

方法描述
lineCap设置或返回线条的结束端点样式
lineJoin设置或返回两条线相交时,所创建的拐角类型
lineWidth设置或返回当前的线条宽度
miterLimit设置或返回最大斜接长度

示例来了

ctx.beginPath(); // 开始绘制
ctx.arc(100, 100, 20, 0, Math.PI * 2, true); // 画圆
var grdRound = ctx.createRadialGradient(50, 80, 30, 0, 120, 120); //创建放射性渐变
grdRound.addColorStop(0, "#CAC531"); // 添加渐变第一个颜色
grdRound.addColorStop(1, "#F3F9A7"); // 添加渐变第二个颜色
ctx.fillStyle = grdRound; // 设置填充样式为渐变色
ctx.shadowColor = "#CAC531"; // 设置阴影颜色
ctx.shadowBlur = 20; // 设置阴影模糊度
ctx.fill();  

ctx.beginPath();
ctx.lineCap = "round"; // 设置线条结束端样式为圆角
ctx.arc(100, 100, 50, Math.PI * 0.1, Math.PI * 0.6); // 画圆
ctx.lineWidth = 10; // 设置线条宽度
ctx.strokeStyle = "#43c6ac"; // 设置线条颜色
ctx.stroke()

ctx.beginPath();
ctx.moveTo(130, 160); // 画圆
ctx.lineTo(210, 320);
ctx.lineWidth = 10;
var grdLine = ctx.createLinearGradient(130, 160, 210, 320); // 创建线性渐变
grdLine.addColorStop(0, "#43c6ac");
grdLine.addColorStop(1, "#CAC531");
ctx.strokeStyle = grdLine; // 设置渐变色
ctx.stroke()

效果如图

额...

更我想象中的不一样

我想画的其实是洛基手上的那根️权杖,正如大家所看见的一样已经非常“接近”了。

更我高呼:"canvas,牛 * "。

言归正传,除了看到一些常用的属性和方法使用,还有没有什么其他启发?在canvas上绘制,就和我们用画笔在白纸上绘制的思路是一样的,比如我想画个红色太阳,我肯定要用红色蜡笔,去画太阳,画完了太阳我想画一颗小草,那这个时候我是不是就要换成绿色的画笔去画小草?


基础篇就这样给大家介绍完了~

现在想想文章最初我说的那段话,入门了基础之后,你现在对一些canvas应用或者效果的实现有思路了吗?嗯?前一秒,感觉自己又学到了,冷静下来一思考,后一秒有感觉啥也想不通。这种感觉糟糕透了。

别急,让我们来看几个实际应用案例,来给大家一些思想启蒙。

四、Canvas应用案例

刮刮乐

先来看看效果

在分析实现思路之前在给大家介绍一个canvas的方法drawImage(),它又三种用法

1.在画布上定位图像:

context.drawImage(img,x,y,width,height);

2.在画布上定位图像,并规定图像的宽度和高度:

context.drawImage(img,x,y,width,height);

3.剪切图像,并在画布上定位被剪切的部分:

context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);

方法在画布上绘制图像、画布或视频。也能够绘制图像的某些部分,以及/或者增加或减少图像的尺寸。

  • img: 规定要使用的图像、画布或视频。
  • sx: 可选。开始剪切的 x 坐标位置。
  • sy: 可选。开始剪切的 y 坐标位置。
  • swidth: 可选。被剪切图像的宽度。
  • sheight: 可选。被剪切图像的高度。
  • x: 在画布上放置图像的 x 坐标位置。
  • y: 在画布上放置图像的 y 坐标位置。
  • width: 可选。要使用的图像的宽度。(伸展或缩小图像)
  • height: 可选。要使用的图像的高度。(伸展或缩小图像)

我们主要使用的是第二种方法,将图片按照canvas的宽高绘制到canvas上,然后将canvas元素绝对定位到我们到内容元素上。

那要怎么刮呢~?文章上面让大家记住clearRect()这个方法,没错我们就是通过这个方法来清除绘制,再结合js的onmousedownonmousemove事件来做到“刮”的这个效果。

😊~~嘴角开始微微上扬~

喝了这碗稀饭,乘还有肚子,再啃个馒头。

黑客代码雨

老规矩,先上效果图

由于录制原因,效果看起来会有卡顿。

那在讲到这个实现之前还是得先给大家介绍个方法fillText(),该方法用于在画布上绘制填色的文本。文本的默认颜色是黑色。

context.fillText(text,x,y,maxWidth);

  • text: 规定在画布上输出的文本。
  • x:开始绘制文本的 x 坐标位置(相对于画布)。
  • y:开始绘制文本的 y 坐标位置(相对于画布)。
  • maxWidth: 可选。允许的最大文本宽度,以像素计。
context.fillText('看', 5, 20);

效果如图

我们在画布上输出文字的同时还可以通过font属性对输入的文字进行设置,例如

context.font="20px Georgia"; // 字体大小设置20px,Georgia字体

那现在我们开始来分析下这个效果:

不管不管三七二十一,动画先不管,我们先把黑色背景设置上~ 没问题吧。

先不管动画,我们先画一行字出来,那怎么画呢?当我们设置了字体的大小之后,根据容器的宽度我们能够计算出需要画多少列,也就是计算出fillText()中我们需要的x的可传参数。这样通过循环一下列数,我们就能画出一行字来?

那我们在通过设置fillText()中的y参数就可以实现高低错落的文字效果?逐渐淡化的效果我们在每次绘制之前先绘制一个容器大小的具有0.05透明度的一个背景的一个矩形,这样是不是每一次绘制就盖住上一次绘制的内容,由于有透明度的影响,上一次绘制的是不是就会暗淡一点。最后一步是让它动起来,我们可以通过setInterval()方法不停的触发绘制,当绘制频率快起来了之后,就变成了动画。

这个案例最好自己动手去写一写更方便理解。

一股作气!还有最后一个案例~

五子棋

效果图:

实现主要有两个难点。

1)如何让棋子画在格子点上

2)怎么判断输赢

如何画棋盘我就不多说了,还是让我们的脑子稍微动一动~

首先我们先要设置棋盘的格子间距常量:DISTANCE。那我们先按照这个间距将棋盘画好。

绘制棋子位置

通过设置canvas元素的点击事件来画棋子,画棋子就是画圆,上面已经说过了,绘画的位置是关键,我们可以获取到触发点击事件为坐标(event.offsetX,event.offsetY) ,那如何根据点击事件获取绘制的坐标点?

我们可以至少可以线获取到距离当前触发点最近的左侧的x坐标吧,是不是下面这样:

const left_x = Math.floor( event.offsetX / DISTANCE ) * DISTANCE // 向下取整

那多出来距离就是

const d = event.offsetX - Math.floor( event.offsetX / DISTANCE ) * DISTANCE ;

这个距离肯定是小于一个格子的长度的,那这样我们就可以根据偏移量四舍五入将棋子x点确定下来

const x = Math.round( d / DISTANCE ) * DISTANCE + left_x;

同理我们也可以算出y的坐标点,这样就可以画棋子了。

我们每画一个棋子,将坐标点分别保存进对应的数组中 white_list 和 black_list;

判断输赢

主要有三种情况

  • 水平方向
  • 垂直方向
  • 斜线方向

主要是四个线性函数

这应该是你最会做的线性函数了吧~~!!

每次落子的时候将棋子的坐标分别带进去计算,从保存起来的棋子数列中找出满足当前线性函数的五个棋子,然后再去判断棋子是否五个连起来的就行了!

什么?怎么判断五个棋子是不是连起来的? 真行,张嘴就来~。

小脑袋瓜子转起来~~

终篇


剑,已经给你们,就看你们自己怎么耍了!


最后送诸君一句话:

待君剑法超然时,但愿青丝仍勃发。



可算是写完了,希望能对大家有所帮助。

作者也是第一次发文章,如文中有什么错误或者建议欢迎大家指正~

参考资料:

w3school canvas

如何使用canvas制作出炫酷的背景特效