Canvas绘图作为可视化的一种实现方式,很是有趣.尤其那些富有想象力以及具有优秀审美的人,往往可以用它呈现堪称艺术的效果.然而像我这种菜鸟,只能从简单的模仿开始了!
言归正传,今天主要针对canvas绘图学习的初级者,分享一下自己完成上图动态可交互'粒子-线'的过程!虽然设计也不完美,代码实现也还有纰漏,但效果还可以一下是不是!(假装是吧可怜可怜我/捂脸)
分析
在实现一个功能之前,我们需要分析一下整个功能包括那些部分,实现逻辑流程又是怎样的,最后再去编码,过程中进行些许微调,以达到理想的效果.那么实现如上图的效果,需要怎么做呢?主要分硬件和软件两部分.
硬件
- 画布:创建canvas标签,并指定宽高
- 画笔:通过2d绘图环境拿到画笔
软件
有了纸和笔,那画点什么?
- 动画
- 粒子:需要满足一下要求:
- 粒子为圆形
- 大小和位置不固定
- 在画布范围内移动
- 碰触边界后折返
- 连接线:当两个粒子靠近至一定范围内时连接两个粒子;超出范围,连接线消失
- 大小变化:势力范围内的粒子越多,粒子的半径越大
-
鼠标交互:鼠标在画布范围内移动时,绘制粒子并同样连线,以实现交互
编码实现
再坚持一下下,我很快说玩的!下面是编码实现的主要流程:
界面布局 --> 粒子属性和行为抽象 + 工具类 --> 初始化(创建画布/画笔和粒子)
--> 粒子动起来 --> 粒子间连线 --> 鼠标交互 --> OK
界面布局
基本就没有布局,就是最简单的页面,只是要设置body
撑满页面.
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Particle Liine</title>
<script src="../js/particle.line.js"></script>
<script src="../js/utils.js"></script>
<style>
body {
margin: 0;
padding: 0;
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<script>
// 相关初始化代码
// 创建画布和画笔 创建粒子 循环动画等
</script>
</body>
</html>
工具类
主要包括生成随机数/计算距离/合并属性以及定义属性(defProp
).其中定义属性类似C#
中的get/set
,可以实现在属性值放生变化时对新值进行检查以及触发一些钩子函数.
//*********** Utils
const PI = Math.PI,
PI_2 = 2 * PI;
/**
* 随机数
* @param {Number} min
* @param {Number} max 最大值
*/
function random(min = 0, max = 1, round = true) {
if (round) return Math.round(Math.random() * (max - min) + min);
else return Math.random() * (max - min) + min;
}
/**
* 计算两点距离
* @param {Array} from 起点
* @param {Array} to 终点
*/
function distance(from = [0, 0], to = [0, 0]) {
const disX = from[0] - to[0],
dsiY = from[1] - to[1];
return Math.sqrt(disX * disX + dsiY * dsiY);
}
/**
* 合并属性
*/
function merage(target, source) {
for (const key in source) {
if (source.hasOwnProperty(key)) {
const element = source[key];
target[key] = element;
}
}
}
/**
* 定义属性
* @param {Object} target 目标对象
* @param {String} key 属性名
*/
function defProp(target) {
return function(key, defaultValue, config) {
let value = defaultValue;
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: () => {
if (config.gettter) {
const rel = config.gettter(value);
return rel || value;
}
return value;
},
set: nVal => {
config.setter
? (() => {
const rel = config.setter(nVal);
value = rel ? rel : nVal;
})()
: (() => {
throw `Property '${key}' of ${target.toString()} can't be changed!`;
})();
}
});
};
}
粒子Particle
因为粒子具有一些可以抽象的属性和行为,所以我们把粒子封装成一个类Particle
,主要属性包括做标位置/半径/样式/活动范围/速度/方向/势力范围等属性,以及步进和绘图等行为.下边是具体实现:
/**
* 粒子类
*/
function Particle() {
this.x = 0;
this.y = 0;
this.radius = 5;
this.style = {
lineWidth: 1,
fillStyle: `hsla(${random(0, 360)},50%,30%,0.8)`
};
this.range = new Extent();
this.vx = 1;
this.vy = 1;
// 柯理化
this.defProp = defProp(this);
// 方向
this.defProp("direction", 0, {
setter: nVal => {
this.vx = this.speed * Math.cos(nVal);
this.vy = this.speed * Math.sin(nVal);
}
});
// 速度
this.defProp("speed", 1, {
setter: nVal => {
this.vx = nVal * Math.cos(this.direction);
this.vy = nVal * Math.sin(this.direction);
}
});
// 势力范围
// 与半径成正比
this.defProp("bossbom", 50, {
gettter: value => {
return this.radius * 20;
}
});
}
/**
* 步进
*/
Particle.prototype.step = function() {
if (!this.range.isIn(this.x, this.y)) {
// 碰撞边界后折返
switch (this.range.craash(this.x, this.y)) {
case 1:// 上
case 3:// 下
this.vy *= -1;
break;
case 2:// 右
case 4:// 左
this.vx *= -1;
}
}
this.x += this.vx;
this.y += this.vy;
};
/**
* 绘图
*/
Particle.prototype.draw = function(ctx) {
ctx.save();
ctx.beginPath();
merage(ctx, this.style);
ctx.arc(this.x, this.y, this.radius, 0, PI * 2, false);
ctx.fill();
ctx.restore();
};
粒子在移动过程中,需要判断是否在绘图范围内以及是否与哪个边界发生了碰撞,所以抽象出Extent
类,以实现相关功能!
范围Extent
/**
* 范围
*/
function Extent(from = [0, 0], to = [100, 100]) {
this.minX = Math.min(from[0], to[0]);
this.minY = Math.min(from[1], to[1]);
this.maxX = Math.max(from[0], to[0]);
this.maxY = Math.max(from[1], to[1]);
}
// 是否在范围内
Extent.prototype.isIn = function(x, y) {
return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY;
};
// 判断与那个边界发生碰撞
// 上右下左 -> 1 2 3 4
Extent.prototype.craash = function(x, y) {
if (x <= this.minX) {
return 4;
} else if (x >= this.maxX) {
return 2;
} else if (y >= this.maxY) {
return 3;
} else if (y <= this.minY) {
return 1;
}
};
初始化:创建画布/画笔和粒子
好了 马上就能看到效果了
// 获取界面宽高
const width = document.body.clientWidth,
height = this.document.body.clientHeight,
offset = 4;
// 创建画布
let oCanvas = this.document.createElement("canvas"),
// 获取画笔
ctx = oCanvas.getContext("2d"),
// 粒子数
particleCount = 30,
// 存放粒子的数组
particleAry = [];
// 设置画布宽高
oCanvas.width = width;
oCanvas.height = height;
// 将画布添加至页面
this.document.body.appendChild(oCanvas);
// 创建粒子
for (let i = 0; i < particleCount; i++) {
let pc = new Particle();
pc.x = random(offset, width - offset);
pc.y = random(offset, height - offset);
pc.radius = random(4, 8);
pc.oldRadius = pc.radius;
pc.range = new Extent([offset, offset], [width - offset, height - offset]);
pc.direction = random(0, PI * 2, false);
pc.speed = 2 / pc.radius;
pc.style.fillStyle = "rgba(200,200,200,0.5)";
particleAry.push(pc);
pc.draw(ctx);
}
动画
下边就让他们动起来...,通过requestAnimationFrame
可以实现流程动画,因为浏览器内部对动画执行做了优化;不建议使用setTimeInterval
,因为这是个不靠谱的家伙!
// 执行
loop();
/**
* 动画循环
*/
function loop() {
// 这里很关键
requestAnimationFrame(loop);
// 擦除画布
ctx.clearRect(0, 0, width, height);
// 执行步进操作
step();
}
// 步进
function step() {
particleAry.forEach(pc1 => {
pc1.step();
// 判断势力范围
let partners = 0; // 记录势力范围内的粒子数
particleAry.forEach(partner => {
if (pc1 != partner) {
if (distance([pc1.x, pc1.y], [partner.x, partner.y]) <= pc1.bossbom) {
// 绘制连接线
ctx.save();
ctx.beginPath();
ctx.strokeStyle = "rgba(200,200,200,0.5)";
ctx.moveTo(pc1.x, pc1.y);
ctx.lineTo(partner.x, partner.y);
ctx.stroke();
ctx.restore();
partners += 1;
}
}
});
// 根据势力范围内的粒子数改变粒子半径
pc1.radius = pc1.oldRadius + partners*0.5;
});
// 绘制粒子 以免被连线遮盖
particleAry.forEach(pc => {
pc.draw(ctx);
});
}
};
到这里已经可以动起来了,好开森!
鼠标交互
其实很简单,监听鼠标事件.当鼠标移入画布时添加一个例子,在画布内移动时改变例子的位置,移出话不是移除相应粒子!是不是很简单呢?
let mousePc = new Particle();
// 鼠标移入画布时添加粒子
oCanvas.onmouseenter = function(e) {
mousePc.x = e.clientX;
mousePc.y = e.clientY;
mousePc.radius = 8;
// 记录原始半径
mousePc.oldRadius = mousePc.radius;
mousePc.range = new Extent(
[offset, offset],
[width - offset, height - offset]
);
mousePc.speed = 0;
mousePc.style.fillStyle = "rgba(200,100,100,1)";
particleAry.push(mousePc);
};
// 鼠标移出画布时移除粒子
oCanvas.onmouseleave = function() {
const index = particleAry.indexOf(mousePc);
if (index >= 0) particleAry.splice(index, 1);
};
// 鼠标移动时更改粒子位置
oCanvas.onmousemove = function(e) {
mousePc.x = e.clientX;
mousePc.y = e.clientY;
};
最终效果图
参考
- [知乎登录注册页动态离子背景效果-DEMO]http://www.lovebxm.com/canvas-special/zhihu/index.html
如果您感觉有所帮助,或者有问题需要交流,欢迎留言评论,非常感谢!
前端菜鸟,还请多多关照!