Fork me on GitHub

canvas进阶笔记

1.加载图像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<canvas id="mycanvas" width="500" height="500" style="background-color: #ddd"></canvas>
<script>
var canvas = document.getElementById("mycanvas");
var ctx = canvas.getContext("2d");

//1.创建image对象
var image = new Image();
//2.设置image的src
image.src = 'img/plane.png';

//监听image的onload事件,绘制图片到画板
image.onload = function () {
ctx.drawImage(image, 50, 50);
//改变图片的位置和大小
ctx.drawImage(image, 150, 50, 60, 100);
}

</script>

2.图像裁切

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<canvas id="mycanvas" width="500" height="500" style="background-color: #ddd"></canvas>
<script>
var canvas = document.getElementById("mycanvas");
var ctx = canvas.getContext("2d");

//但是有时候我们并不需要使用完整的图像,而只是图像的一部分内容,这个时候我们就需要使用图像裁剪
//context.drawImage(image, source_x, source_y, source_width, source_height, x, y, width, heigh);
/*
image:源图像对象
source_x:源图像的裁剪区原点横坐标
source_y:源图像的裁剪区原点纵坐标
source_width:源图像的裁剪区宽度
source_height:源图像的裁剪区高度
x:在画布上绘制图像的原点横坐标
y:在画布上绘制图像的原点纵坐标
width:在画布上绘制图像的宽度
heigh:在画布上绘制图像的高度
*/

//举例:
var image = new Image();
image.src = 'http://coding.imweb.io/img/p3/retina-pixel.jpg';
image.onload = function () {
ctx.drawImage(image, 300, 300, 180, 180);
//加载图片后,边截取图片且缩放展示在画布左上角
ctx.drawImage(image, 260, 260, 480, 480, 0, 0, 240, 240);
}

</script>

3.requestAnimationFrame

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
<canvas id="mycanvas" width="500" height="500" style="border: solid 1px #ddd"></canvas>
<script>
/*
如果我们需要实现动画,只需要设置定时不断地绘制下一帧的画面便可以了。
在 JavaScript 中我们可以使用 setTimeout 和 setInterval 来设置延时任务。
因此在很长时间以来,计时器一直都是 JavaScript 动画的最核心技术。
如下面的代码就是使用 setTimeout 方法来实现基本的动画循环:
function animate() {
// 动画内容
animation1();
animation2();
// 间隔100ms执行动画循环
setTimeout(function () {
animate();
}, 100);
}
// 执行动画
animate();

早期的动画循环时候,最关键的问题是确定循环间隔的时长。一方面,循环间隔必须足够短,这样才能动画效果显得更平滑流畅;
另一方面,循环时隔还要足够长,这样才能确保浏览器有能力渲染产生的变化。我们知道大多数的显示器的刷新频率是 60Hz ,
即相当于在每秒钟中屏幕会重绘 60 次。因此最平滑动画的最佳循环间隔是 1000ms/60,约等于 16.7ms。
*/
/*
*setTimeout 和 setInterval 问题

无论是 setTimeout 和 setInterval 都并不是十分精准。
我们知道 JavaScript 是一个单线程的解释器,在一定时间能只能执行一段代码。
为了要控制代码的执行顺序,就需要通过一个 JavaScript任务队列 来进行管理控制(任务会按照添加到队列的顺序执行)。
通过 setTimeout 和 setInterval 我们能够设置延时多长时间把我们的代码任务添加到 JavaScript任务队列 中。
如果当前任务队列是空的,那么添加的代码可以立即执行;如果队列不是空的,则新添加的任务需要等到其前面所有的任务都执行完成才能执行。
由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,setTimeout 和 setInterval 指定的任务,一定会按照预定时间执行。

如下面的代码:
setTimeout(animateTask, 1000 / 60);
// 耗时长的任务
longTimeTask();

上面代码的 setTimeout,我们制定 16.7ms 后运行 animateTask 任务。
但是,如果由于后面的 longTimeTask 执行(当前脚本的同步任务))非常耗时,
即使过了 16.7ms 仍无法结束,那么延迟执行的 animateTask 就只有等着,只有等到前面的任务都运行完,才能轮到它执行。

*/

/*
requestAnimationFrame
由于 setTimeout 和 setInterval 的不精准问题,促使了 requestAnimationFrame 的诞生。
requestAnimationFrame 是专门为实现高性能的帧动画而设计的一个API,目前已在多个浏览器得到了支持,你可以把它用在 DOM 上的效果切换或者 Canvas 画布动画中。
requestAnimationFrame 并不是定时器,但和 setTimeout 很相似,在没有 requestAnimationFrame 的浏览器一般都是用setTimeout模拟。
requestAnimationFrame 跟屏幕刷新同步(大多数是 60Hz )。
如果浏览器支持 requestAnimationFrame , 则不建议使用 setTimeout 来做动画。

下面是我们常规使用 requestAnimationFrame 的兼容写法,当浏览器不兼容的 requestAnimationFrame 时则通过使用 setTimeout 来模拟实现,且设定渲染间隔为 1000ms/60。
// 判断是否有 requestAnimationFrame 方法,如果有则模拟实现
window.requestAnimFrame =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, 1000 / 30);
};
*/

//举例:下面我们将使用 requestAnimationFrame 来实现一个物体来回移动的动画
var canvas = document.getElementById("mycanvas");
var ctx = canvas.getContext("2d");

window.requestAnimFrame =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 30);
};

//绘制的圆的对象
var circle = {
//圆心坐标
x: 250,
y: 250,
//半径
radius: 50,
//移动方向
direction: 'right',
//移动圆形
move: function () {
if (this.direction === 'right') {
if (this.x <= 430) {
this.x += 5;
} else {
this.direction = 'left';
}
} else {
if (this.x >= 60) {
this.x -= 5;
} else {
this.direction = 'right';
}
}
},
draw: function () {
//绘制图形
ctx.beginPath();
//设置开始角度为0,结束角度为2π弧度
ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
ctx.fillStyle = '#00c09b';
ctx.fill();
}
}

//动画执行函数
function animate() {
// 随机更新圆形位置
circle.move();
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘画圆
circle.draw();

// 使用requestAnimationFrame实现动画循环
requestAnimationFrame(animate);
}

//执行第一帧的元,即初始化的圆
circle.draw();
//执行animate
animate();
</script>

4.动画循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<canvas id="mycanvas" width="500" height="500" style="border: solid 1px #ddd"></canvas>
<script>
var canvas = document.getElementById("mycanvas");
var ctx = canvas.getContext("2d");

//1.初始化元素
var monster = {
x: 30,
y: 50,
size: 60,
speed: 5
}
//初始化画面
var image = new Image();
image.src = 'img/enemy.png';
image.onload = function () {

ctx.drawImage(image, monster.x, monster.y, monster.size, monster.size);
}

//动画函数,利用requestAnimationFrame去执行
function animate() {
if (monster.x > 400) {
return;
}
monster.x += monster.speed;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(image, monster.x, monster.y, monster.size, monster.size)
requestAnimationFrame(animate);
}

animate();
</script>

5.canvas键盘处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<canvas id="mycanvas" width="500" height="500" style="border: solid 1px #ddd"></canvas>
<script>
/**
* 键盘事件处理
在制作 PC 端的游戏的时候,我们经常需要监听键盘的事件,以便响应用户的键盘操作。
目前,对键盘事件的支持主要遵循的是 DOM0级。

按键相关事件
键盘操作涉及下面三种事件:
keydown:当用户按下键盘上的任意键时触发,而且如果按住按住不放的话,会重复触发此事件。
keypress:当用户按下键盘上的字符键时触发,而且如果按住不放的,会重复触发此事件(按下Esc键也会触发这个事件)。
keyup:当用户释放键盘上的键时触发。

按键过程:
用户按下键盘上的字符键时
首先会触发 keydown 事件
然后紧接着触发 keypress 事件
最后触发 keyup事件
如果用户按下了一个字符键不放,就会重复触发 keydown 和 keypress 事件,直到用户松开该键为止。

键码(keyCode)对照表
在发送 keydown 和 keyup 事件时,event 对象的 keyCode 属性中会包含一个代码,与键盘上一个特定的键对应。
如下图,为我们键盘键位的 keyCode 对照表:

*/
//举例:简单实现键盘控制物体移动
// window.requestAnimFrame =
// window.requestAnimationFrame ||
// window.webkitRequestAnimationFrame ||
// window.mozRequestAnimationFrame ||
// window.oRequestAnimationFrame ||
// window.msRequestAnimationFrame ||
// function(callback) {
// window.setTimeout(callback, 1000 / 30);
// };

var canvas = document.getElementById("mycanvas");
var ctx = canvas.getContext("2d");

var rect = {
x: 290, // 矩形的 x 坐标
y: 400, // 矩形的 y 坐标
width: 60, // 矩形的宽度
height: 100, // 矩形的高度
speed: 100, // 矩形移动的步伐
direction: 0,//移动方向
timePointer: 0, //时间戳
keyValue:-1,
onpress:false
}

document.onkeydown = function (e) {
console.log(11);
// 获取被按下的键值 (兼容写法)

var key = e.keyCode || e.which || e.charCode;
if(rect.keyValue==-1&&rect.onpress==false) {
rect.keyValue = key;
rect.onpress = true;
} else {
return;
}

rect.timePointer = new Date().getTime();
rect.xPointer= rect.x;
switch (key) {
// 点击左方向键
case 37:
rect.direction = -1;

break;
// 点击右方向键
case 39:
rect.direction = 1;
break;
}

}

document.onkeyup = function (e) {
var key = e.keyCode || e.which || e.charCode;
if(rect.keyValue==key){
rect.direction = 0;
rect.keyValue = -1;
rect.onpress = false;
}


}
var image = new Image();
image.src = 'img/plane.png';
image.onload = function () {
ctx.drawImage(image, rect.x, rect.y, rect.width, rect.height)
};
function drawRect() {
//清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 获取当前时间
var nowTime = new Date().getTime();
//获取时间间隔
var t = (nowTime-rect.timePointer)/1000;
// console.log(t);
rect.timePointer = nowTime;
//计算位移
rect.x += rect.direction*rect.speed*t;

ctx.drawImage(image, rect.x, rect.y, rect.width, rect.height);
requestAnimationFrame(drawRect);
}

//第一次绘制
drawRect();

</script>

6.碰撞检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<canvas id="mycanvas" width="500" height="500" style="border: solid 1px #ddd"></canvas>
<script>
/*
在实现 Canvas 游戏和动画中,往往需要解决物体相互碰撞的情况如。
对于物体碰撞相关的问题,我们会在动画中采用 碰撞检测 来解决,以此实现更为逼真的动画。

碰撞检测关键步骤
碰撞检测需要处理经历下面两个关键的步骤:
计算判断两个物体是否发生碰撞
发生碰撞后,两个物体的状态和动画效果的处理
计算碰撞:只要两个物体相互接触,它们就会发生碰撞。

我们可以看到 矩形2 和 矩形1 之间没有发生碰撞共有四种可能的情况:
矩形2的右侧 离 矩形1的左侧有一段距离
矩形2的左侧 离 矩形1的右侧有一段距离
矩形2的底部 离 矩形1的顶部有一段距离
矩形2的顶部 离 矩形1的底部有一段距离
当符合上面其中一种情况,则两个矩形没有发生碰撞。
因此通过逆向推导我们可以得出:当上面四种情况都不满足的时候,则代表两个矩形碰撞了。
在代码中,我们可以这样写:
// 判断四边是否都没有空隙
if (!(rect2.x + rect2.width < rect1.x) &&
!(rect1.x + rect1.width < rect2.x) &&
!(rect2.y + rect2.height < rect1.y) &&
!(rect1.y + rect1.height < rect2.y)) {
// 物体碰撞了
}

圆形物体碰撞检测:
假设发生碰撞的物体是圆形时,检测碰撞则变得比较复杂了,前面矩形所使用的碰撞检测,并不能判断圆形物体的情况。
如下图的情况:

那么如何检测两圆是否碰撞了呢?这个时候又到了考验我们数理化的知识了。
检测两圆是否相交:当两个圆心之间的距离是否小于两个圆的半径之和。这是已经被证实的数学运算。

其中 dx 和 dy 分别表示两个圆之间的横坐标和纵坐标的差值。 即 dx = x2 - x1; dy = y2 - y1;
然后我们需要通过 勾股定理 计算两个圆心之间的距离。如下图:

因此我们碰撞检测的代码可以这样写:

var dx = circle2.x - circle1.x;
var dy = circle2.y - circle1.y;
var distance = Math.sqrt((dx * dx) + (dy * dy));
if (distance < circle1.radius + circle2.radius) {
// 两个圆形碰撞了
}


碰撞后的处理
当检测到碰撞后,则可以对碰撞的物体进行状态设置了,可以是相互毁灭,或者是反弹等。这里大家可以根据场景来决定。
*/
</script>
`

本文标题:canvas进阶笔记

文章作者:tongtong

发布时间:2017年01月18日 - 22:01

最后更新:2019年03月13日 - 19:03

原始链接:https://ilove-coding.github.io/2017/01/18/canvas进阶笔记/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!
-------------本文结束-------------