第18章 动画与Canvas图形
本章节涉及动画与Canvas,由于web开发较少涉及Canvas和WebGL,所以本章只介绍requestAnimationFrame和Canvas部分。
# requestAnimationFrame
早期使用js来执行动画是通过setTimeout和setInterval实现,不过这种动画的问题在于浏览器不知道js动画何时开始,以及最佳间隔是多少(不同系统有差异)。
一般来说,计算机显示器的屏幕刷新率是60Hz,即每秒绘制60次,所以平滑动画最佳的重绘间隔是1000/60,约16.67ms。
不过浏览器对于setTimeout和setInterval的计时器精度也是大小不一,即使将时间间隔设置为最优,但受限于浏览器的计时器精度,也只能得到近似的结果。
此时requestAnimationFrame应运而生,它会通知浏览器js代码要执行动画了,这样浏览器就可以对动画进行适当优化。
<!DOCTYPE html>
<head>
<style>
.progress{
width: 0;
height: 100px;
background-color: pink;
}
</style>
</head>
<body>
<button>button</button>
<div class="progress"></div>
</body>
</html>
<script>
const btn = document.querySelector('button')
const progress = document.querySelector('.progress')
const updateProgress =()=>{
progress.style.width = (parseInt(progress.style.width,10)+1) + '%'
if(progress.style.width!=='100%'){
requestAnimationFrame(updateProgress)
}
}
btn.onclick = function(){
progress.style.width = '0%'
requestAnimationFrame(updateProgress)
}
</script>
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
requestAnimationFrame会返回一个请求id,可以通过canceAnimationFrame来取消重绘任务。
# 通过requestAnimationFrame节流
requestAnimationFrame可以保证每次重绘只调用一次回调函数,可以达到节流的效果。对于频繁发生的行为(如页面滚动),可以进行节流来提升性能。
const log = ()=>{
console.log('Invoked at: ',Date.now())
}
window.addEventListener('scroll',()=>{
requestAnimationFrame(log)
})
2
3
4
5
6
# Canvas
canvas是一个html标签,用于绘制图形。绘制时是绘制在画布上,此时有个对象叫上下文,通过它调取canvas的API来进行绘制。
const canvas = document.querySelector('canvas')
let ctx = canvas.getContext('2d')
2
2D上下文的坐标原点(0,0)在<canvas>元素的左上角,x坐标向右增长,y坐标向下增长。
# 填充和描边
fillStyle: 填充颜色,可以是CSS支持的任意格式,默认"#000000"。
strokeStyle: 描边颜色,格式同上。
# 绘制矩形
绘制矩形的方法有:
- fillRect(): 用于以指定颜色在画布上绘制并填充矩形。
- strokeRect():在画布上绘制矩形轮廓。
- clearRect():擦除画布中某个区域。
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')
// 绘制蓝色矩形
ctx.fillStyle = 'blue'
ctx.fillRect(10,10,50,50)
// 绘制红色描边矩形
ctx.strokeStyle = 'red'
ctx.strokeRect(30,30,50,50)
// 在重叠区擦除一个矩形区
ctx.clearRect(40,40,10,10)
2
3
4
5
6
7
8
9
10
11
12
13
# 绘制路径
- beginPath():创建新路径。
- arc(x,y,radius,startAngle,endAngle,counterclockwise):绘制弧线。
- arcTo(x1,y1,x2,y2,radius):绘制弧线。
- bezierCurveTo(c1x,c1y,c2x,c2y,x,y):绘制弧线(三次贝塞尔曲线)。
- lineTo(x,y):绘制到(x,y)的直线。
- moveTo(x,y):不绘制,画笔移到(x,y)。
- quadraticCurveTo(cx,cy,x,y):绘制弧线(二次贝塞尔曲线)。
- rect(x,y,width,height):绘制矩形路径。
- closePath():绘制一条返回起点的路径。
路径绘制完成后,可以使用fill()方法填充,也可以使用stroke()来描边。
下面的例子绘制了一个不带数字的表盘:
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// 创建路径
ctx.beginPath()
// 绘制外圆
ctx.arc(100, 100, 99, 0, 2 * Math.PI, false)
// 绘制内圆
ctx.moveTo(194,100)
ctx.arc(100,100,94,0,2*Math.PI,false)
// 绘制分针
ctx.moveTo(100,100)
ctx.lineTo(100,15)
// 绘制时针
ctx.moveTo(100,100)
ctx.lineTo(35,100)
// 描边
ctx.stroke()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 绘制文本
fillText(): 填充文字。
strokeText():描边文字。
ctx.font = '30px Arial'
ctx.fillText('hello',100,120)
ctx.strokeText('world',100,140)
2
3
有一个辅助确定文本大小的measureText()
方法,可以返回文本的宽度。下面的例子演示了字体大小从100px开始计算,不断递减,直到将文本刚好放到对应像素宽度的矩形中。
function setSize(text,maxWidth=140){
let fontSize = 100
ctx.font = fontSize + 'px Arail'
while(ctx.measureText(text).width>maxWidth){
fontSize--
ctx.font = fontSize + 'px Arail'
}
}
setSize('Hello world',140)
ctx.fillText('Hello world',10,100)
setSize('Hello world',280)
ctx.fillText('Hello world',10,200)
2
3
4
5
6
7
8
9
10
11
12
13
14
# 变换
以下方法用于改变绘制上下文的变换矩阵。
- rotate(angle):围绕原点把图像旋转angle弧度。
- scale(scaleX,scaleY),缩放。
- translate(x,y):把原点移动到(x,y)。
- transform(m1_1,m1_2,m2_1,m2_2,dx,dy):通过矩阵乘法直接修改矩阵。
上面表盘的例子中,可以改动如下:
ctx.translate(100,100)
// 绘制分针
ctx.moveTo(0,0)
ctx.lineTo(0,15-100)
// 绘制时针
ctx.moveTo(0,0)
ctx.lineTo(35-100,0)
2
3
4
5
6
7
8
9
即将原点平移到表盘中心,之后的坐标都-100即可。
在变换过程中,变换的状态可以保存和弹出。
save():保存当前上下文的属性和状态,此时的设置会被暂存到栈中。
restore():应用完当前状态后,恢复(回滚)到上一个状态。
# 绘制图像
drawImage(img,x,y,width,height,targetX,targetY,targetWidth,targetHeight):把图像或canvas绘制到画布上,前3个参数必填。
# 阴影(略)
非常规绘图,参考阴影 (opens new window)。
# 渐变(略)
非常规绘图,参考渐变 (opens new window)。
# 图像数据(略)
可以通过getImageData() (opens new window)获取原始图像数据,比如RGB的大小信息。并可以通过putImageData()重新绘制。
# 合成(略)
有2个属性可以全局应用:globalAlpha (opens new window)和globalCompositeOperation (opens new window)。