前言
没有最好, 只有更好, 如题所示, 这篇文章只要是分享一个用 Canvas 来实现的粒子运动效果. 感觉有点标题党了, 但换个角度, 勉勉强强算是炫丽吧, 虽然色彩上与炫丽无关, 但运动效果上还是算得上有点点炫的. 不管怎么样, 我们还是开始这个所谓的炫丽效果吧!
实现代码
直接上代码 , 不懂可以看代码注释. 估计就会看明白大概的思路了.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Canvas 实现炫丽的粒子运动效果 - 云库前端 </title>
<style>
* {
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
height: 100%;
}
canvas {
display: block;
background: #000;
}
body::-webkit-scrollbar{
display: none;
}
.operator-box{
position: fixed;
top: 0;
left: 50%;
border: 1px solid #fff;
background: rgba(255,255,255,0.5);
padding: 20px 10px;
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
}
.back-type,.back-animate{
margin-right: 20px;
}
.flex-box{
display: flex;
justify-content: center;
align-items: center;
}
#input-text{
line-height: 35px;
width: 260px;
height: 35px;
background: rgba(0, 0, 0,0.7);
color: #fff;
font-size: 16px;
border: none;
outline: none;
text-indent: 12px;
box-shadow: inset 0 0 12px 1px rgba(0,0,0,0.7);
}
#input-text::placeholder{
color: #ccc;
line-height: 55px;
height: 55px;
}
select{
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: none;
padding: 0px 20px 0px 6px;
height: 35px;
color: #fff;
text-align: left;
background: rgba(0, 0, 0,0.7) url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAICAYAAAAx8TU7AAAAOUlEQ...R4gPgWEIMAiOYBCS4C8ZDAIrBq4gigNkztQEFMi6AuQHESAPMeXiEMiWfpAAAAAElFTkSuQmCC) no-repeat 190px 12px;
background-size: 5px 8px;
box-shadow: inset 0 0 12px 1px rgba(0,0,0,0.7);
}
</style>
</head>
<body>
<div class="operator-box">
<div class="flex-box">
<select name=""id="selectType">
<option value="back"> 归位 </option>
<option value="auto"> 随机 </option>
</select>
</div>
<select class="back-dynamics" id="selectDynamics">
<option value="spring">dynamics.spring</option>
<option value="bounce">dynamics.bounce</option>
<option value="forceWithGravity">dynamics.forceWithGravity</option>
<option value="gravity">dynamics.gravity</option>
<option value="easeInOut">dynamics.easeInOut</option>
<option value="easeIn">dynamics.easeIn</option>
<option value="easeOut">dynamics.easeOut</option>
<option value="linear">dynamics.linear</option>
</select>
</div>
<div class="input-box"><input type="text" placeholder="输入汉字后回车" id="input-text"></div>
</div>
</div>
<script src="dynamics.min.js"></script>
<script src="index.js"></script>
<script>
var iCircle = new Circle();
</script>
</body>
</html>
HTML 代码不多, 只要是几个操作元素. 这里一看就明白. 不费过多口舌. 我们来看看本文的主角 JavaScript 代码, 不过, 在看代码前, 我们不妨先听听实现这个效果的思路:
首先, 我们得先生成一堆群众演员 (粒子);
把每个粒子的相关参数挂到自身的一些属性上, 因为第个粒子都会有自己的运动轨迹;
接着得让它们各自运动起来. 运动有两种 (自由运动和生成文字的运动);
JavaScript 代码中使用了三个 Canvas 画布, this.iCanvas(主场),this.iCanvasCalculate(用来计算文字宽度),this.iCanvasPixel(用于画出文字, 并从中得到文字对应的像素点的位置坐标).
this.iCanvasCalculate 和 this.iCanvasPixel 这两个无需在页面中显示出来, 只是辅助作用.
美美的 JavaScript 代码
/*
* @Author: 朝夕熊
* @Date: 2017-11-05 10:17:45
* @Last Modified by: 朝夕熊
* @Last Modified time: 2017-11-12 14:12:14
*/
function Circle() {
var This = this;
this.init();
this.generalRandomParam();
this.drawCircles();
this.ballAnimate();
this.getUserText();
// 窗口改变大小后, 生计算并获取画面
window.onresize = function() {
This.stateW = document.body.offsetWidth;
This.stateH = document.body.offsetHeight;
This.iCanvasW = This.iCanvas.width = This.stateW;
This.iCanvasH = This.iCanvas.height = This.stateH;
This.ctx = This.iCanvas.getContext("2d");
}
}
// 初始化
Circle.prototype.init = function() {
// 父元素宽高
this.stateW = document.body.offsetWidth;
this.stateH = document.body.offsetHeight;
this.iCanvas = document.createElement("canvas");
// 设置 Canvas 与父元素同宽高
this.iCanvasW = this.iCanvas.width = this.stateW;
this.iCanvasH = this.iCanvas.height = this.stateH;
// 获取 2d 绘画环境
this.ctx = this.iCanvas.getContext("2d");
// 插入到 body 元素中
document.body.appendChild(this.iCanvas);
this.iCanvasCalculate = document.createElement("canvas");
// 用于保存计算文字宽度的画布
this.mCtx = this.iCanvasCalculate.getContext("2d");
this.mCtx.font = "128px 微软雅黑";
this.iCanvasPixel = document.createElement("canvas");
this.iCanvasPixel.setAttribute("style", "position:absolute;top:0;left:0;");
this.pCtx = null; // 用于绘画文字的画布
// 随机生成圆的数量
this.ballNumber = ramdomNumber(1000, 2000);
// 保存所有小球的数组
this.balls = [];
// 保存动画中最后一个停止运动的小球
this.animte = null;
this.imageData = null;
this.textWidth = 0; // 保存生成文字的宽度
this.textHeight = 150; // 保存生成文字的高度
this.inputText = ""; // 保存用户输入的内容
this.actionCount = 0;
this.ballActor = []; // 保存生成文字的粒子
this.actorNumber = 0; // 保存生成文字的粒子数量
this.backType = "back"; // 归位
this.backDynamics = ""; // 动画效果
this.isPlay = false; // 标识 (在生成文字过程中, 不能再生成)
}
// 渲染出所有圆
Circle.prototype.drawCircles = function() {
for (var i = 0; i < this.ballNumber; i++) {
this.renderBall(this.balls[0]);
}
}
// 获取用户输入文字
Circle.prototype.getUserText = function() {
This = this; // 保存 this 指向
ipu = document.getElementById("input-text");
ipu.addEventListener("keydown",
function(event) {
if (event.which === 13) { // 如果是回车键
ipu.value = ipu.value.trim(); // 去头尾空格
var pat = /[\u4e00-\u9fa5]/; // 中文判断
var isChinese = pat.test(ipu.value);
if (ipu.value.length != 0 && isChinese) {
This.inputText = ipu.value;
} else {
alert("请输入汉字");
return;
}
if (This.isPlay) {
return
}
This.getAnimateType();
This.getTextPixel();
This.isPlay = true;
}
});
}
// 计算文字的宽
Circle.prototype.calculateTextWidth = function() {
this.textWidth = this.mCtx.measureText(this.inputText).width;
}
// 获取文字像素点
Circle.prototype.getTextPixel = function() {
if (this.pCtx) {
this.pCtx.clearRect(0, 0, this.textWidth, this.textHeight);
}
this.calculateTextWidth(this.inputText);
this.iCanvasPixel.width = this.textWidth;
this.iCanvasPixel.height = this.textHeight;
this.pCtx = this.iCanvasPixel.getContext("2d");
this.pCtx.font = "128px 微软雅黑";
this.pCtx.fillStyle = "#FF0000";
this.pCtx.textBaseline = "botom";
this.pCtx.fillText(this.inputText, 0, 110);
this.imageData = this.pCtx.getImageData(0, 0, this.textWidth, this.textHeight).data;
this.getTextPixelPosition(this.textWidth, this.textHeight);
}
// 获取文字粒子像素点位置
Circle.prototype.getTextPixelPosition = function(width, height) {
var left = (this.iCanvasW - width) / 2;
var top = (this.iCanvasH - height) / 2;
var space = 4;
this.actionCount = 0;
for (var i = 0; i < this.textHeight; i += space) {
for (var j = 0; j < this.textWidth; j += space) {
var index = j * space + i * this.textWidth * 4;
if (this.imageData[index] == 255) {
if (this.actionCount < this.ballNumber) {
this.balls[this.actionCount].status = 1;
this.balls[this.actionCount].targetX = left + j;
this.balls[this.actionCount].targetY = top + i;
this.balls[this.actionCount].backX = this.balls[this.actionCount].x;
this.balls[this.actionCount].backY = this.balls[this.actionCount].y;
this.ballActor.push(this.balls[this.actionCount]);
this.actionCount++;
}
}
}
this.actorNumber = this.ballActor.length;
}
this.animateToText();
}
// 粒子运动到指定位置
Circle.prototype.animateToText = function() {
for (var i = 0; i < This.actorNumber; i++) {
dynamics.animate(This.ballActor[i], {
x: this.ballActor[i].targetX,
y: this.ballActor[i].targetY
},
{
type: dynamics.easeIn,
duration: 1024,
});
}
setTimeout(function() {
This.ballbackType();
},
3000);
}
// 粒子原路返回
Circle.prototype.ballBackPosition = function() {
for (var i = 0; i < This.actorNumber; i++) {
var ball = This.ballActor[i];
dynamics.animate(ball, {
x: ball.backX,
y: ball.backY
},
{
type: dynamics[this.backDynamics],
duration: 991,
complete: this.changeStatus(ball)
});
}
}
// 获取类型 | 动画效果
Circle.prototype.getAnimateType = function() {
var selectType = document.getElementById("selectType");
var selectDynamics = document.getElementById("selectDynamics");
this.backType = selectType.options[selectType.options.selectedIndex].value;
this.backDynamics = selectDynamics.options[selectDynamics.options.selectedIndex].value;
}
// 复位散开
Circle.prototype.ballbackType = function() {
if (this.backType == "back") {
this.ballBackPosition();
} else {
this.ballAutoPosition();
}
this.ballActor = [];
}
// 随机散开
Circle.prototype.ballAutoPosition = function(ball) {
for (var i = 0; i < this.actorNumber; i++) {
this.changeStatus(this.ballActor[i])
}
}
// 更改小球状态
Circle.prototype.changeStatus = function(ball) {
ball.status = 0;
if (this.isPlay == true) {
this.isPlay = false;
}
}
// 随机生成每个圆的相关参数
Circle.prototype.generalRandomParam = function() {
for (var i = 0; i < this.ballNumber; i++) {
var ball = {};
ball.size = 1; // 随机生成圆半径
// 随机生成圆心 x 坐标
ball.x = ramdomNumber(0 + ball.size, this.iCanvasW - ball.size);
ball.y = ramdomNumber(0 + ball.size, this.iCanvasH - ball.size);
ball.speedX = ramdomNumber( - 1, 1);
ball.speedY = ramdomNumber( - 1, 1);
this.balls.push(ball);
ball.status = 0;
ball.targetX = 0;
ball.targetY = 0;
ball.backX = 0;
ball.backY = 0;
}
}
// 改变圆的位置
Circle.prototype.changeposition = function() {
for (var i = 0; i < this.ballNumber; i++) {
if (this.balls[i].status == 0) {
this.balls[i].x += this.balls[i].speedX;
this.balls[i].y += this.balls[i].speedY;
}
}
}
// 画圆
Circle.prototype.renderBall = function(ball) {
this.ctx.fillStyle = "#fff";
this.ctx.beginPath(); // 这个一定要加
this.ctx.arc(ball.x, ball.y, ball.size, 0, 2 * Math.PI);
this.ctx.closePath(); // 这个一定要加
this.ctx.fill();
}
// 小球碰撞判断
Circle.prototype.collision = function(ball) {
for (var i = 0; i < this.ballNumber; i++) {
if (ball.x > this.iCanvasW - ball.size || ball.x < ball.size) {
if (ball.x > this.iCanvasW - ball.size) {
ball.x = this.iCanvasW - ball.size;
} else {
ball.x = ball.size;
}
ball.speedX = -ball.speedX;
}
if (ball.y > this.iCanvasH - ball.size || ball.y < ball.size) {
if (ball.y > this.iCanvasH - ball.size) {
ball.y = this.iCanvasH - ball.size;
} else {
ball.y = ball.size;
}
ball.speedY = -ball.speedY;
}
}
}
// 开始动画
Circle.prototype.ballAnimate = function() {
var This = this;
var animateFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; (function move() {
animte = animateFrame(move);
This.ctx.clearRect(0, 0, This.iCanvasW, This.iCanvasH);
This.changeposition();
for (var i = 0; i < This.ballNumber; i++) {
This.collision(This.balls[i]);
This.renderBall(This.balls[i]);
}
})();
}
// 生成一个随机数
function ramdomNumber(min, max) {
return Math.random() * (max - min) + min;
}
看了代码估计也只是心里炫了一下, 也没有让你想把这个东西做出来的欲望, 为此我知道必需得让你眼睛心服口服才行. 在线 DEMO: yunkus.com/demo/canvas... .
人无完人, 代码也一样. 看起来运行顺畅的代码也或多或少有一些瑕疵, 日前这个效果还只支持中文. 英文的话, 我得再努力一把, 不管怎么样, 英文后面肯定是会加入来的, 只是时间问题了. 还有代码中用于标记是否可再次执行生成文字的 属性: this.isPlay , 还是一点瑕疵, this.isPlay 的状态更改没有准确的在粒子归位的那一瞬间更改, 而是提前更改了状态. 但这个状态不会影响本例子效果的完整实现.
这个例子中用到了 dynamics.js 库, 主要是用到它里面的一些运动函数, 让粒子动起来更感人一些, 仅此而已.
来源: https://juejin.im/post/5a707089518825732821b9dd