先看效果
说干就干
1.先定义个粒子类 Particle
class Parcicle {
constructor(props) {
// 粒子的横坐标初始位置
this.x = 0;
// 粒子的纵坐标初始位置
this.y = 0;
// 粒子的横向速度
this.vx = 0;
// 粒子的纵向速度
this.vy = 0;
// 粒子的运动终点横坐标
this.finalX = 0;
// 粒子的运动终点纵坐标
this.finalY = 0;
// 粒子的半径
this.r = 1;
// 粒子的填充颜色
this.fillStyle = '#000';
// 粒子的描边颜色
this.strokeStyle = '#000';
Object.assign(this, props);
}
render(ctx) {
const { x, y, r, fillStyle, strokeStyle } = this;
ctx.save();
ctx.translate(x, y);
ctx.fillStyle = fillStyle;
ctx.strokeStyle = strokeStyle;
ctx.beginPath();
ctx.arc(0, 0, r, 0, 2 * Math.PI);
ctx.stroke();
ctx.fill();
ctx.restore();
return this;
}
}
复制代码
2.定义粒子系统类
先确定下实例化参数
let fontList = [
{
// 要显示的文字
font: '3',
// 字体颜色
color: 'green',
// 字体显示时间
lifetime: 2000,
// 字体占比画布大小比例
size: 0.8,
// 粒子密度
density: 5
}
];
new ParticleSystem({
fontDataList: fontList,
// 循环显示
loop: true,
// canvas宽度
width: 200,
// canvas 高度
height: 200,
// 字体占比画布大小比例
size: 0.8,
// 画布容器
container
});
复制代码
3.设置默认值
constructor(props) {
// 显示的字体样式
this.font = '?';
// 当前显示字体的颜色
this.color = 'red';
// canvas容器
this.container = null;
// canvas 的宽度
this.width = 200;
// canvas 的高度
this.height = 200;
this.size = 0.8;
//粒子未扩散之前的范围
this.range = 0.5;
//粒子密度
this.density = 4;
Object.assign(this, props);
this.init();
return this;
}
复制代码
4.初始化调用
init() {
// 设置画布尺寸
this.setCanvasSize();
// 执行动画
this.animate();
// 添加画布到页面元素
this.appendCanvas();
}
复制代码
5.动画执行方法
animate() {
const _this = this;
// 获取当前时间戳
let startTime = +new Date();
(function move() {
const { currentFontIndex, fontDataList } = _this;
// 获取执行动画时的时间
let endTime = +new Date();
let interTime = endTime - startTime;
let lifetime = fontDataList[currentFontIndex] && fontDataList[currentFontIndex].lifetime;
// 判断当前时间差是否达到预设的lifeTime值来判断是否执行下一个文字
if (interTime >= lifetime) {
_this.nextFont();
startTime = +new Date();
}
_this.drawParticle();
requestAnimationFrame(move);
})();
}
复制代码
6.文字切换
nextFont() {
const { fontDataList, loop } = this;
const _this = this;
// 设置当前字体的配置
Object.entries(fontDataList[this.currentFontIndex]).forEach(([ key, value ]) => {
_this[key] = value || _this.defaultValue(key);
});
if (typeof _this.font == 'string') _this.setFont();
this.currentFontIndex += 1;
if (fontDataList.length <= this.currentFontIndex) {
if (!loop) return false;
this.currentFontIndex = 0;
}
}
setFont() {
const { height, width, size, font, ctx } = this;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#000';
// 预设10px的字体
ctx.font = 'blod 10px Arial';
// 测量10px字体的宽高
const measure = ctx.measureText(font);
// 10px的字体除行高7得到字体的高度
const lineHeight = 7;
// 通过画布的尺寸计算出最大的字体尺寸
const fSize = Math.min(height * size * 10 / lineHeight, width * size * 10 / measure.width);
ctx.save();
ctx.font = `bold ${fSize}px Arial`;
const measureResize = ctx.measureText(font);
const left = (width - measureResize.width) / 2;
const bottom = (height + fSize / 10 * lineHeight) / 2;
ctx.fillText(font, left, bottom);
// 获取回执好的图片信息
this.getImageData();
ctx.restore();
}
复制代码
7.获取图片文字信息
getImageData() {
const { ctx, color, range, width, height, density } = this;
// 获取图片信息
const data = ctx.getImageData(0, 0, width, height);
const particleList = [];
for (let x = 0, width = data.width, height = data.height; x < width; x += density) {
for (let y = 0; y < height; y += density) {
// 获取粒子的初始位置
const currentX = (width - width * range) / 2 + Math.random() * width * range;
const currentY = (height - height * range) / 2 + Math.random() * height * range;
const i = (y * width + x) * 4;
// 如果当前的像素点有色值则push当前的像素点到particleList
if (data.data[i + 3]) {
particleList.push(
new Particle({
x: currentX,
y: currentY,
finalX: x,
finalY: y,
fillStyle: color,
strokeStyle: color
})
);
}
}
}
this.particleList = particleList;
this.drawParticle();
}
复制代码
8.绘制粒子执行动画
drawParticle() {
const { particleList, ctx, width, height } = this;
if (!particleList) return false;
ctx.clearRect(0, 0, width, height);
// 设置加速度系数,参数越大动画执行速度越快
const spring = 0.01;
// 设置摩擦系数,让粒子最后能停下来(偷了个懒没做终点位置校正)
const FRICTION = 0.88;
// 让所有的粒子都动起来吧
particleList.forEach((item, index) => {
item.vx += (item.finalX - item.x) * spring;
item.vy += (item.finalY - item.y) * spring;
item.x += item.vx;
item.y += item.vy;
item.vx *= FRICTION;
item.vy *= FRICTION;
item.render(ctx);
});
}
复制代码
至此粒子已经完成了动画
8.添加图片支持
别着急,为了让粒子系统支持图片转化粒子还得再加点东西
setImage() {
const { height, width, font, ctx } = this;
ctx.clearRect(0, 0, width, height);
ctx.drawImage(font, 0, 0, width, height);
this.getImageData();
}
复制代码
第六步的nextFont也需要再添加个判断
if (typeof _this.font == 'object') _this.setImage();
复制代码
加载图片是异步行为,但是配合 es7 的async,await 又能省了不少麻烦。不说了直接上代码。
function loadImg(src) {
return new Promise((res, rej) => {
let oImg = new Image();
oImg.src = src;
oImg.onload = () => res(oImg);
oImg.onerror = (err) => rej(err);
});
}
(async () => {
const oImg4 = await loadImg('./1.png');
let fontList = [
{
font: '3',
color: 'green',
lifetime: 2000,
size: 0.8,
density: 5
},
{
font: oImg4,
lifetime: 2000,
density: 5,
color: "red"
}
];
const container = document.getElementById('canvasContainer');
new ParticleSystem({
fontDataList: fontList,
loop: true,
width: 200,
height: 200,
size: 0.8,
container
});
})();
复制代码
源码地址: github.com/tomatoKnigh…