一、引言
这是一个全新的系列,在这个系列中,我将通过一系列实际案例,和你一起学习、研究Canvas
,从而掌握编写 Canvas
特效的能力。
HTML5
canvas
元素用于图形的绘制,通过脚本 (通常是JavaScript
)来完成。
canvas
标签只是图形容器,您必须使用脚本来绘制图形。
你可以通过多种方法使用canvas
绘制路径,盒、圆、字符以及添加图像。
这是一个注重实际案例的系列教程,我希望能够通过具体的案例,而非抽象的文档和解释带你学习 Canvas
这项技术。那么废话不多说,我们直接看这次的案例吧。
二、案例介绍
这是在很多博客网站上都非常常见的背景特效,在空旷的画布上,有非常多的随机运动的点,这些点在运动时如果距离足够近,则会产生连线。当鼠标移入时,也会为周围的点创建连线,同时与鼠标相连的点会受到鼠标的牵引。
我们可以将这个案例轻松地划分为三个由易到难的实现阶段:
- 简单效果:即完成点的自由运动、以及连线的自动产生。
- 复杂效果:鼠标移入时,完成鼠标与周围点的连线。
- 最终效果:鼠标移动时,与鼠标相连的点受到牵引。
在学习完这篇文章后,我建议你至少掌握到第二阶段,最终效果是否能够掌握取决于你是否有兴趣。
三、逐步实现
1. 文件创建
首先,创建一个html
文件:
<!DOCTYPE html>
<html lang="cn">
<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">
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
<title>Document</title>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
同时,创建对应的CSS
文件:
*{
margin: 0;
padding: 0;
}
body{
background-color: #333;
overflow: hidden;
}
#canvas{
position:fixed;
left:0;
top:0;
}
最后,创建一个js
文件,接下来,我们将在js
文件中进行编辑。
2. 简单效果的实现
因为这是本系列的第一期,因此我会比较啰嗦地讲一些 Canvas
的方法调用。
首先,我们需要创建一个 canvas
对象。
var canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var ctx = canvas.getContext("2d");
首先,我们需要找到 Canvas
对象,然后为 Canvas
对象设置宽高。
接下来,我们需要获取到 Canvas
的 context
对象。getContext("2d")
对象是内建的 HTML5
对象,拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。我们在进行绘制时,需要调用它上面的属性和方法。
var particles = []
var count = parseInt(canvas.height/150*canvas.width/150);
var maxDis = 150;
接下来,我们创建一个particles
数组,用来存储所有的粒子对象,然后规定粒子的数目在每150*150
像素一颗,最后定义连线的最大距离是150
像素。
在完成了上面一系列准备动作以后,我们需要设计一个 Particle
类。
class Particle{
constructor(x,y) {
this.x = x;
this.y = y;
this.directionY = 0.5 - Math.random();
this.directionX = 0.5 - Math.random();
}
}
我们首先定义了Particle
类的构造方法,它定义了位置x
、y
,速度变量directionX
、directionY
。
接下来,我们需要设计一个更新方法,用于更新粒子下一步的状态:
update() {
this.x += this.directionX;
this.y += this.directionY;
}
最后,我们需要为粒子对象设计一个draw方法,draw函数里直接调用了canvas的方法,用于在视图上绘制粒子:
draw() {
ctx.beginPath();
ctx.arc(this.x,this.y,2,0,Math.PI*2);
ctx.fillStyle = "white";
ctx.fill();
}
这个方法中,我们使用了一开始拿到的context
对象,利用这个对象上的属性和方法绘制粒子。在绘制粒子时,我们首先调用ctx.beginPath();
表示开始绘制,接下来调用ctx.arc(this.x,this.y,2,0,Math.PI*2);
在粒子所在的位置绘制一个半径为2
的圆,最后使用ctx.stroke();
表明绘制结束。
在canvas中绘制圆形, 我们将使用以下方法:
arc(x,y,r,start,stop)
通过以上几个方法,我们就完成了Particle
类的定义,完整的代码如下:
class Particle{
constructor(x,y) {
this.x = x;
this.y = y;
this.directionY = 0.5 - Math.random();
this.directionX = 0.5 - Math.random();
}
update() {
this.x += this.directionX;
this.y += this.directionY;
}
draw() {
ctx.beginPath();
ctx.arc(this.x,this.y,2,0,Math.PI*2);
ctx.fillStyle = "white";
ctx.fill();
}
}
接下来,我们需要实现一个创建粒子的方法:
function createParticle(){
let x = Math.random() * canvas.width;
let y = Math.random() * canvas.height;
particles.push(new Particle(x, y));
}
我们每创建一个粒子,就将粒子放进我们的particles
数组里。
下面,我们实现一个处理粒子的方法:
function handleParticle(){
particles.forEach((element,index) => {
element.update();
element.draw();
if(element.x < 0 || element.x > canvas.width){
element.directionX = - element.directionX;
}
if(element.y < 0 || element.y > canvas.height) {
element.directionY = - element.directionY;
}
particles.forEach((aElement,index) => {
distance = Math.sqrt( Math.pow(element.x - aElement.x,2) + Math.pow(element.y - aElement.y,2) );
if(distance < maxDis) {
ctx.beginPath();
ctx.strokeStyle = "rgba(255,255,255," + (1 - distance / maxDis) + ")";
ctx.moveTo(element.x,element.y);
ctx.lineTo(aElement.x,aElement.y);
ctx.lineWidth = 1;
ctx.stroke();
}
})
});
}
在粒子的处理函数中,我们通过forEach
函数遍历每个粒子,然后分别调用粒子的update
方法和draw
方法,更新粒子的状态,并且绘制粒子。之后再做检查,如果粒子超出了canvas
元素的边界,则通过改变它的速度方向使其反弹回去。
之后,我们还需要再进行一次内部遍历,检查当前粒子与其他粒子的距离,如果距离小于maxDis
,则绘制粒子之间的线段。绘制的颜色可以将透明度设置为与距离有关,绘制的方法与上面的draw
函数有异曲同工之妙,这里就不再啰嗦。
最后,我们通过调用一个定时器来完成粒子的动态效果:
function draw(){
ctx.clearRect(0,0,canvas.width,canvas.height);
//清空画布内容
if(particles.length < count) {
createParticle();
}
handleParticle();
}
setInterval(draw,10);
至此,我们已经完成了简单效果,即完成点的自由运动、以及连线的自动产生。这是实现了简单效果的完整代码:
var canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var ctx = canvas.getContext("2d");
var particles = []
var count = parseInt(canvas.height/150*canvas.width/150);
var maxDis = 150;
class Particle{
constructor(x,y) {
this.x = x;
this.y = y;
this.directionY = 0.5 - Math.random();
this.directionX = 0.5 - Math.random();
}
update() {
this.x += this.directionX;
this.y += this.directionY;
}
draw() {
ctx.beginPath();
ctx.arc(this.x,this.y,2,0,Math.PI*2);
ctx.fillStyle = "white";
ctx.fill();
}
}
function createParticle(){
let x = Math.random() * canvas.width;
let y = Math.random() * canvas.height;
particles.push(new Particle(x, y));
}
function handleParticle(){
particles.forEach((element,index) => {
element.update();
element.draw();
if(element.x < 0 || element.x > canvas.width){
element.directionX = - element.directionX;
}
if(element.y < 0 || element.y > canvas.height) {
element.directionY = - element.directionY;
}
particles.forEach((aElement,index) => {
distance = Math.sqrt( Math.pow(element.x - aElement.x,2) + Math.pow(element.y - aElement.y,2) );
if(distance < maxDis) {
ctx.beginPath();
ctx.strokeStyle = "rgba(255,255,255," + (1 - distance / maxDis) + ")";
ctx.moveTo(element.x,element.y);
ctx.lineTo(aElement.x,aElement.y);
ctx.lineWidth = 1;
ctx.stroke();
}
})
});
}
function draw(){
ctx.clearRect(0,0,canvas.width,canvas.height);
if(particles.length < count) {
createParticle();
}
handleParticle();
}
setInterval(draw,10);
3. 复杂效果的实现
实现简单效果以后,接下来,我们将演示如何实现复杂一些的效果,即鼠标移入时,完成鼠标与周围点的连线。
这个功能其实不难,在实现时最重要的在于获取当前的鼠标位置。
这里我们选择使用全局变量mouseX
、mouseY
来保存鼠标信息。同时设定一个鼠标捕获最大距离maxMouseDis
。
var mouseX = -1,mouseY = -1;
var maxMouseDis = 250;
同时,通过为canvas
对象添加如下的鼠标事件,
canvas.addEventListener("mousemove",function(e) {
mouseX = e.clientX;
mouseY = e.clientY;
})
canvas.addEventListener("mouseout",function(){
mouseX = -1;
mouseY = -1;
})
当鼠标在canvas
元素上移动时,会实时更新鼠标位置,一旦鼠标移出canvas
元素,我们就将mouseX
和mouseY
置为-1
作为标记。
添加完鼠标事件后,我们还需要设计一个鼠标处理函数:
function handleMouse(){
if(mouseX == -1 || mouseY == -1) return;
particles.forEach((element,index) => {
let distance = Math.sqrt( Math.pow(element.x - mouseX,2) + Math.pow(element.y - mouseY,2) );
if(distance < maxMouseDis) {
ctx.beginPath();
ctx.strokeStyle = "rgba(255,255,255," + (1 - distance / maxMouseDis) + ")";
ctx.moveTo(element.x,element.y);
ctx.lineTo(mouseX,mouseY);
ctx.lineWidth = 1;
ctx.stroke();
}
})
}
在handleMouse
函数中,我们需要遍历所有的粒子,一旦鼠标与粒子之间的距离小于设定的maxMouseDis
,则开始连线,连线方式与上面handleParticle
中一致。
最后,不要忘记在定时器函数draw
中添加handleMouse
:
function draw(){
ctx.clearRect(0,0,canvas.width,canvas.height);
if(particles.length < count) {
createParticle();
}
handleParticle();
handleMouse();
}
通过上面一系列操作,我们已经完成了复杂效果,即鼠标移入时,完成鼠标与周围点的连线。这是完整的代码:
var canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var ctx = canvas.getContext("2d");
var particles = []
var count = parseInt(canvas.height/150*canvas.width/150);
var maxDis = 150;
var maxMouseDis = 250;
var mouseX = -1,mouseY = -1;
class Particle{
constructor(x,y) {
this.x = x;
this.y = y;
this.directionY = 0.5 - Math.random();
this.directionX = 0.5 - Math.random();
}
update() {
this.x += this.directionX;
this.y += this.directionY;
}
draw() {
ctx.beginPath();
ctx.arc(this.x,this.y,2,0,Math.PI*2);
ctx.fillStyle = "white";
ctx.fill();
}
}
function createParticle(){
let x = Math.random() * canvas.width;
let y = Math.random() * canvas.height;
particles.push(new Particle(x, y));
}
function handleParticle(){
particles.forEach((element,index) => {
element.update();
element.draw();
if(element.x < 0 || element.x > canvas.width){
element.directionX = - element.directionX;
}
if(element.y < 0 || element.y > canvas.height) {
element.directionY = - element.directionY;
}
particles.forEach((aElement,index) => {
distance = Math.sqrt( Math.pow(element.x - aElement.x,2) + Math.pow(element.y - aElement.y,2) );
if(distance < maxDis) {
ctx.beginPath();
ctx.strokeStyle = "rgba(255,255,255," + (1 - distance / maxDis) + ")";
ctx.moveTo(element.x,element.y);
ctx.lineTo(aElement.x,aElement.y);
ctx.lineWidth = 1;
ctx.stroke();
}
})
});
}
function handleMouse(){
if(mouseX == -1 || mouseY == -1) return;
particles.forEach((element,index) => {
let distance = Math.sqrt( Math.pow(element.x - mouseX,2) + Math.pow(element.y - mouseY,2) );
if(distance < maxMouseDis) {
ctx.beginPath();
ctx.strokeStyle = "rgba(255,255,255," + (1 - distance / maxMouseDis) + ")";
ctx.moveTo(element.x,element.y);
ctx.lineTo(mouseX,mouseY);
ctx.lineWidth = 1;
ctx.stroke();
}
})
}
function draw(){
ctx.clearRect(0,0,canvas.width,canvas.height);
if(particles.length < count) {
createParticle();
}
handleParticle();
handleMouse();
}
setInterval(draw,10);
canvas.addEventListener("mousemove",function(e) {
mouseX = e.clientX;
mouseY = e.clientY;
})
canvas.addEventListener("mouseout",function(){
mouseX = -1;
mouseY = -1;
})
4. 最终效果的实现
最终效果是要实现鼠标移动时,与鼠标相连的点受到牵引。听起来有点困难,但实际只需要在handleMouse
函数中做文章。
考虑这样一个场景:当鼠标移入Canvas
元素时,与之相邻的粒子会形成与鼠标的连线,可以把连线考虑为有弹性的绳子。但由于连线本身并没有力的作用,因此粒子会继续沿着它原来的方向移动。直到粒子与鼠标间的距离大于某个值,粒子会受到线的牵引,从而在一瞬间受到一个连线方向的力,粒子的移动速度也发生了变化。
因此我们规定,连线在maxMouseDis / 3 * 2
以内不具有弹性,在maxMouseDis / 3 * 2
与maxMouseDis
之间具有弹性,会将粒子向鼠标牵引。
因此,我们需要在handleMouse中添加如下代码段:
let velocity = Math.sqrt( Math.pow(element.directionX,2) + Math.pow(element.directionY,2) );
if(distance > maxMouseDis / 3 * 2) {
element.directionX = - velocity * (element.x - mouseX) / distance * 1;
element.directionY = - velocity * (element.y - mouseY) / distance * 1;
}
当达到牵引的条件时,我们就保持粒子的速度不变,改变它的速度方向为朝向鼠标的方向。
这样一番修改以后,已经实现了粒子被鼠标捕获并受到牵引的效果。
但这样的效果让人感觉不太真实。因为仅仅是改变了粒子原有速度的方向,当鼠标移动时,就没法实现粒子被鼠标拉动的效果。因此还可以这样优化:
var maxVelocity = 0.6;
var lastMouseX = -1,lastMouseY = -1;
var mouseVelocity = 0;
首先添加最大粒子速度、上一刻的鼠标位置和鼠标移速三个全局变量。
if(lastMouseX == -1 || lastMouseY == -1) {
mouseVelocity = 0;
} else {
mouseVelocity = Math.sqrt( Math.pow(lastMouseX - mouseX,2) + Math.pow(lastMouseY - mouseY,2) )
}
lastMouseX = mouseX;
lastMouseY = mouseY;
随后在鼠标移动的事件侦听器中,添加如上代码,即可得到当前的鼠标移速。
if(distance > maxMouseDis / 3 * 2) {
element.directionX = - Math.min(Math.max(mouseVelocity,velocity),maxVelocity) * (element.x - mouseX) / distance * 1;
element.directionY = - Math.min(Math.max(mouseVelocity,velocity),maxVelocity) * (element.y - mouseY) / distance * 1;
}
最后在handleMouse
函数中通过如上的代码对牵引效果进行改进。牵引时的粒子速度将取鼠标速度和粒子本身速度的较大者,但不能超过预定的最大速度。
通过上面一系列修改,我们完成了最终效果,这是完整的代码:
var canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var ctx = canvas.getContext("2d");
var particles = []
var count = parseInt(canvas.height/150*canvas.width/150);
var maxDis = 150;
var maxMouseDis = 250;
var maxVelocity = 0.6;
var mouseX = -1,mouseY = -1;
var lastMouseX = -1,lastMouseY = -1;
var mouseVelocity = 0;
class Particle{
constructor(x,y) {
this.x = x;
this.y = y;
this.directionY = 0.5 - Math.random();
this.directionX = 0.5 - Math.random();
}
update() {
this.x += this.directionX;
this.y += this.directionY;
}
draw() {
ctx.beginPath();
ctx.arc(this.x,this.y,2,0,Math.PI*2);
ctx.fillStyle = "white";
ctx.fill();
}
}
function createParticle(){
let x = Math.random() * canvas.width;
let y = Math.random() * canvas.height;
particles.push(new Particle(x, y));
}
function handleParticle(){
particles.forEach((element,index) => {
element.update();
element.draw();
if(element.x < 0 || element.x > canvas.width){
element.directionX = - element.directionX;
}
if(element.y < 0 || element.y > canvas.height) {
element.directionY = - element.directionY;
}
particles.forEach((aElement,index) => {
distance = Math.sqrt( Math.pow(element.x - aElement.x,2) + Math.pow(element.y - aElement.y,2) );
if(distance < maxDis) {
ctx.beginPath();
ctx.strokeStyle = "rgba(255,255,255," + (1 - distance / maxDis) + ")";
ctx.moveTo(element.x,element.y);
ctx.lineTo(aElement.x,aElement.y);
ctx.lineWidth = 1;
ctx.stroke();
}
})
});
}
function handleMouse(){
if(mouseX == -1 || mouseY == -1) return;
particles.forEach((element,index) => {
let distance = Math.sqrt( Math.pow(element.x - mouseX,2) + Math.pow(element.y - mouseY,2) );
if(distance < maxMouseDis) {
ctx.beginPath();
ctx.strokeStyle = "rgba(255,255,255," + (1 - distance / maxMouseDis) + ")";
ctx.moveTo(element.x,element.y);
ctx.lineTo(mouseX,mouseY);
ctx.lineWidth = 1;
ctx.stroke();
let velocity = Math.sqrt( Math.pow(element.directionX,2) + Math.pow(element.directionY,2) );
if(distance > maxMouseDis / 3 * 2) {
element.directionX = - Math.min(Math.max(mouseVelocity,velocity),maxVelocity) * (element.x - mouseX) / distance * 1;
element.directionY = - Math.min(Math.max(mouseVelocity,velocity),maxVelocity) * (element.y - mouseY) / distance * 1;
}
}
})
}
function draw(){
ctx.clearRect(0,0,canvas.width,canvas.height);
if(particles.length < count) {
createParticle();
}
handleParticle();
handleMouse();
}
setInterval(draw,10);
canvas.addEventListener("mousemove",function(e) {
mouseX = e.clientX;
mouseY = e.clientY;
if(lastMouseX == -1 || lastMouseY == -1) {
mouseVelocity = 0;
} else {
mouseVelocity = Math.sqrt( Math.pow(lastMouseX - mouseX,2) + Math.pow(lastMouseY - mouseY,2) )
}
lastMouseX = mouseX;
lastMouseY = mouseY;
})
canvas.addEventListener("mouseout",function(){
mouseX = -1;
mouseY = -1;
})
四、简单总结
这次,我们实现了这个粒子背景效果。也了解了Canvas
的强大之处。
利用Canvas
实现连续动画往往参考以下思路:
使用一个计时器,每刻完成对应的清除画布、更新元素状态和重绘工作。通过这样的反复计算渲染,即可实现连续的Canvas
动效。
相信通过这一期的教学,你已经掌握了Canvas
的大致用法,建议马上趁热打铁去自己动手试一试!
这个系列将持续更新下去。下一期我将继续借助实战案例与你一起研究Canvas
前端特效,每周六准时更新,欢迎关注本专栏!