前言:
虽然早就听说了Canvas,但是一直没有找到应用场景,所以也没有专门去学习,最近想了解下webgis,就学习了相关的知识,也就学习了Canvas了。二者的关系见链接。
Canvas的文档很全面,所以这里仅仅总结了学习过程中遇到的一些关键问题,以及手写的一些案例(Canvas的API很简单,但是做成复杂的功能就需要js+实战经验了)
学习资料
重难点
1px线的显示效果问题
我的理解:Canvas的线是有粗度的,所以不像数学里把两条线连起来就可以了。而像素是最小不可分割的点,所以当画一条从(100, 100)到(200, 100)的直线时,这两个点的连线即中轴线,然后 1px 的宽度在两边各占了一半,所以为了补齐整个像素点,会有半个像素被染色,整体的宽度也变成了 2px,然后总体颜色也变浅了。
var ctx = document.getElementById("myCanvas").getContext('2d')
ctx.beginPath()
ctx.moveTo(100, 100)
ctx.lineTo(200, 100)
ctx.strokeStyle = 'red'
ctx.stroke()
而画(100, 100.5),(200, 100.5)时刚好在100到101之间,占了1px,也不会占到别的像素点。所以就是1px啦
var ctx = document.getElementById("myCanvas").getContext('2d')
ctx.beginPath()
ctx.moveTo(100, 100.5)
ctx.lineTo(200, 100.5)
ctx.strokeStyle = 'red'
ctx.stroke()
beginPath与closePath
beginPath用于在绘制多条线段且分别设置线段样式时,开辟新路径。否则前后的线段的样式会互相影响。而且这种影响不统一,比如strokeRect画矩形,前后的样式不会影响;而rect画矩形前后样式就会影响
所以最佳实践就是**在画每一个图之前都要设置 beginPath()
**
closePath不是关闭beginPath开启的路径。,而是用于闭合路径。当用线段构成图形时,最后闭合最好用closePath,而不要用lineTo去回到起点:
cxt.moveTo(50, 50)
cxt.lineTo(200, 50)
cxt.lineTo(200, 120)
cxt.lineTo(50, 120)
cxt.lineTo(50, 50) // 需要闭合,又或者使用 closePath() 方法进行闭合,推荐使用 closePath()
cxt.stroke()
几组API分类整理
画线段
// 起点
cxt.moveTo(50, 50)
// 经过的点
cxt.lineTo(200, 50)
cxt.lineTo(200, 120)
// 线的颜色
ctx.strokeStyle = 'green'
// 线帽
ctx.lineCap = 'round'
// 线宽
ctx.lineWidth = 20
cxt.stroke()
画矩形
// strokeRect() 描边矩形
cxt.strokeStyle = 'pink'
// 线宽
cxt.lineWidth = 20
// strokeRect(x, y, width, height) 方法 分别对应左上角坐标 宽、高
cxt.strokeRect(50, 50, 200, 100)
// fillRect() 描边矩形
// 填充颜色
cxt.fillStyle = 'pink'
// 线宽 这个属性就没用了
cxt.lineWidth = 20
// fillRect(x, y, width, height) 方法 分别对应左上角坐标 宽、高
cxt.fillRect(50, 50, 200, 100)
画圆
案例
1.炫彩小球
动画的逻辑:清除原动画、渲染新动画…… 不停地清除、渲染,实现动画
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>炫彩小球</title>
</head>
<body>
<canvas width="800" height="500" id="mycanvas" style="border:1px solid red"></canvas>
<script>
const canvas = document.getElementById('mycanvas')
const ctx = canvas.getContext('2d')
console.log(ctx)
// 设置初始球类
class Ball {
static ballArr = []
constructor(x, y, r) {
this.x = x
this.y = y
this.r = r
this.color = this.getRandom()
// 设置行进方向
this.dx = parseInt(Math.random() * 10) - 5
this.dy = parseInt(Math.random() * 10) - 5
// 将创建的小球存到数组中
Ball.ballArr.push(this)
}
// 获取随机颜色
getRandom () {
const allType = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f']
let color = '#'
for (let i = 0; i < 6; i++) {
let random = Math.floor(Math.random() * 16)
color += allType[random]
}
return color
}
// 渲染小球
render () {
console.log('渲染')
ctx.beginPath()
ctx.arc(this.x, this.y, this.r, 0, 360 * Math.PI / 180, false)
ctx.fillStyle = this.color
ctx.closePath()
ctx.fill()
}
// 让小球动起来
update () {
// 小球随机移动
this.x += this.dx
this.y += this.dy
this.r -= 0.5
if (this.r <= 0) {
this.remove()
}
}
remove () {
for (let i = 0; i < Ball.ballArr.length; i++) {
if (Ball.ballArr[i].r <= 0) {
Ball.ballArr.splice(i, 1)
}
}
}
}
// canvas设置鼠标监听
canvas.addEventListener('mousemove', function (e) {
Ball.ballArr.push(new Ball(e.offsetX, e.offsetY, 30))
})
// 定时器进行动画渲染和更新
setInterval(() => {
// 动画:清屏-更新-渲染
ctx.clearRect(0, 0, canvas.width, canvas.height)
for (let i = 0; i < Ball.ballArr.length; i++) {
Ball.ballArr[i].render()
Ball.ballArr[i].update()
}
// Ball.ballArr.splice(0, Ball.ballArr.length - 1)
}, 50);
</script>
</body>
</html>
2. 小球连线+碰壁折返
碰壁折返的算法核心:判断圆心与canvas边界的距离,改变dx、dy的值,即改变下一个圆的渲染位置,从而实现往反方向走
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>小球连线-碰壁折返</title>
<style>
#mycanvas {
margin: 5px auto;
}
</style>
</head>
<body>
<canvas id="mycanvas" style="border:1px solid red"></canvas>
<script>
const canvas = document.getElementById('mycanvas')
let ctx = canvas.getContext('2d')
// 设置画布尺寸——整个页面.注意这个-10,是为了避免小球有一半在外面
canvas.width = document.documentElement.clientWidth - 10
canvas.height = document.documentElement.clientHeight - 10
// 创建小球类
class Ball {
static ballArr = []
constructor() {
this.x = parseInt(Math.random() * canvas.width)
this.y = parseInt(Math.random() * canvas.height)
this.r = 30
// this.r = r
this.color = this.getRandom()
// 设置行进方向
this.dx = parseInt(Math.random() * 20) - 10
this.dy = parseInt(Math.random() * 20) - 10
// 将创建的小球存到数组中
Ball.ballArr.push(this)
this.index = Ball.ballArr.length - 1
}
// 获取随机颜色
getRandom () {
const allType = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f']
let color = '#'
for (let i = 0; i < 6; i++) {
let random = Math.floor(Math.random() * 16)
color += allType[random]
}
return color
}
// 渲染小球
render () {
console.log('渲染')
ctx.beginPath()
// 透明度
ctx.globalAlpha = 1
// 画小球
ctx.arc(this.x, this.y, this.r, 0, 360 * Math.PI / 180, false)
ctx.fillStyle = this.color
// ctx.closePath()
ctx.fill()
// 判断距离来画两点之间的线
for (let i = this.index + 1; i < Ball.ballArr.length; i++) {
let dis = this.judgeDis(this, Ball.ballArr[i])
if (dis < 300) {
ctx.strokeStyle = this.getRandom()
ctx.beginPath()
ctx.lineWidth = 10
// 根据已经连线的球距离改变线的透明度
ctx.globalAlpha = 1 - parseInt(dis / 300)
ctx.moveTo(this.x, this.y)
ctx.lineTo(Ball.ballArr[i].x, Ball.ballArr[i].y)
ctx.closePath()
ctx.stroke()
}
}
}
// 小球的更新
update () {
this.x += this.dx
this.y += this.dy
// 所谓反弹,也不过如此啦
if (this.x <= this.r || this.x > canvas.width - this.r) {
this.dx = -this.dx
}
if (this.y <= this.r || this.y > canvas.height - this.r) {
this.dy = -this.dy
}
}
// 判断距离
judgeDis (A, B) {
let distance = Math.sqrt(Math.pow(A.x - B.x, 2) + Math.pow(A.y - B.y, 2))
return distance
}
}
// 创建100个球
for (let i = 0; i < 100; i++) {
new Ball()
}
// 定时器动画
for (let i = 0; i < Ball.ballArr.length; i++) {
Ball.ballArr[i].render()
}
setInterval(() => {
// 先清屏再画新的点
ctx.clearRect(0, 0, canvas.width, canvas.height)
for (let i = 0; i < Ball.ballArr.length; i++) {
Ball.ballArr[i].render()
Ball.ballArr[i].update()
}
}, 10)
</script>
</body>
</html>