预览
完整项目预览 ---- 预览地址;
粒子效果原理
在 canvas 中, 可以通过 getImageData() 方法来获取像素数据
- ctx.fillStyle = '#ff0000';
- ctx.fillRect(0, 0, 1, 1);
- const imageData = ctx.getImageData(0, 0, 1, 1);
imageData 有三个属性:
data: 数组, 包含了像素信息, 每个像素会有四个长度, 如
[255,0,0,255, ... ,255,127,0,255]
, 分别代表该像素的 RGBA 值
width:imageData 对象的宽
height:imageData 对象的高
首先在 canvas 上写上某种颜色文字, 再去分析像素数据 (比如改像素是否有透明度等), 然后自己记录下该像素点的位置
下例是通过改变像素的数据而重新写出来的文字
- ctx.font = 'bold 40px Arial';
- ctx.textBaseline = 'middle';
- ctx.textAlign = 'center';
- ctx.fillText('你好啊', 60, 20);
- document.querySelector('#button').addEventListener('click',
- function() {
- const imgData = ctx.getImageData(0, 0, 120, 40);
- for (let i = 0; i < imgData.data.length; i += 4) {
- if (imgData.data[i + 3] == 0) continue;
- imgData.data[i] = 255;
- imgData.data[i + 1] = 0;
- imgData.data[i + 2] = 0;
- // imgData.data[i + 3] = 255; 这个代表的是透明度 透明度不变 255 最高 0 最低
- }
- ctx.putImageData(imgData, 120, 0);
- });
这段代码只是示例说明一下, 实际上才不会有人这么脑残去换颜色吧
获取点位置
要获取点的位置, 首先要将字写在画布上, 但是字又不能让别人看到所以可以动态创建一个画布, 这个画布不会 append 到任何节点上, 只会用于写字
const cache = document.createElement('canvas');
将宽高等与展示的画布设置成一样的 (不贴这部分的代码了)
创建一个对象, 用于获取点的位置
- const ShapeBuilder = {
- // 初始化字的对齐方式等, 我认为 middle 与 center 比较好计算一点
- init(width, height){
- this.width = width;
- this.height = height;
- this.ctx = cache.getContext('2d');
- this.ctx.textBaseline = 'middle';
- this.ctx.textAlign = 'center';
- },
- // 获取位置之前必须先要写入文字 这里的 size=40 是默认值
- write(words, x, y, size = 40){
- // 清除之前写的字
- this.ctx.clearRect(0, 0, this.width, this.height);
- this.font = `bold ${size}px Arial`;
- this.ctx.fillText(words, x, y);
- // 记录当前文字的位置, 方便计算获取像素的区域
- this.x = x;
- this.y = y;
- this.size = size;
- this.length = words.length;
- },
- getPositions(){
- // 因为 imgData 数据非常的大, 所以尽可能的缩小获取数据的范围
- const xStart = this.x - (this.length / 2) * this.size,
- xEnd = this.x + (this.length / 2) * this.size,
- yStart = this.y - this.size / 2,
- yEnd = this.y + this.size / 2,
- //getImageData(起点 x, 起点 y, 宽度, 高度);
- data = this.ctx.getImageData(xStart, yStart, this.size * this.length, this.size).data;
- // 间隔 (下面有介绍)
- const gap = 4;
- let positions = [], x = xStart, y = yStart;
- for(var i = 0;i < data.length; i += 4 * gap){
- if(data[i+3] > 0){
- positions.push({x, y});
- }
- x += gap;
- if(x >= xEnd){
- x = xStart;
- y += gap;
- i += (gap - 1) * 4 * (xEnd - xStart);
- }
- }
- return positions;
- }
- }
- ShapeBuilder.init();
关于 gap: 在循环 imgData 数组的时候, 数据量太大可能会造成卡顿, 所以可以使用间隔来获取坐标点的方法不过可能会造成文字部分地方缺失就需要个人来权衡利弊, 自己来调整了
gap 的值必须能被 xEnd-xStart 给整除, 不然会造成获取坐标点错位的后果
关于 canvas 中 middle 与 center 的规则:
- this.ctx.font = 'bold 40px Arial';
- this.ctx.fillText('你好',40 ,20);
效果如下图所示
fillText 设置的坐标点刚好会是整个字的中点, 就是图中 middle 与 center 的交点其实以其它对齐方式也是可以的, 看个人喜好
更多的对齐规则参考 html 5 Canvas 参考手册的文本
创建微粒类
微粒应该随机生成, 然后移动到指定的位置去
微粒类的属性:
自身当前位置 (x,y), 目标位置:(xEnd,yEnd), 自身大小 (size), 自身颜色 (color), 移动快慢 (e)
方法: go(): 每一帧都要移动一段距离, render(): 渲染出微粒 (我用心形的形状)
- class Particle {
- constructor({x, y, size = 2, color, xEnd, yEnd, e = 60} = {}){
- this.x = x;
- this.y = y;
- this.size = size;
- this.color = color || `hsla(${Math.random() * 360}, 90%, 65%, 1)`;
- this.xEnd = xEnd;
- this.yEnd = yEnd;
- // 经过 e 帧之后到达目标地点
- this.e = e;
- // 计算每一帧走过的距离
- this.dx = (xEnd - x) / e;
- this.dy = (yEnd - y) / e;
- }
- go(){
- // 到目的后保持不动 (其实这里也可以搞点事情的)
- if(--this.e <= 0) {
- this.x = this.xEnd;
- this.y = this.yEnd;
- return ;
- }
- this.x += this.dx;
- this.y += this.dy;
- }
- render(ctx){
- this.go();
- // 下面是画出心型的贝塞尔曲线
- ctx.beginPath();
- ctx.fillStyle = this.color;
- ctx.moveTo(this.x + 0.5 * this.size, this.y + 0.3 * this.size);
- ctx.bezierCurveTo(this.x + 0.1 * this.size, this.y, this.x,
- this.y + 0.6 * this.size, this.x + 0.5 *
- this.size, this.y + 0.9 * this.size);
- ctx.bezierCurveTo(this.x + 1 * this.size, this.y + 0.6 *
- this.size, this.x + 0.9 * this.size, this.y,
- this.x + 0.5 * this.size,
- this.y + 0.3 * this.size);
- ctx.closePath();
- ctx.fill();
- return true;
- }
- }
微粒类最基本的属性与方法就是这些, 如果要让粒子更好看一点, 或者更生动一点, 可以自己添加一些属性与方法
具体流程
- const canvas = {
- init(){
- // 设置一些属性
- this.setProperty();
- // 创建微粒
- this.createParticles();
- //canvas 的循环
- this.loop();
- },
- setProperty(){
- this.ctx = studio.getContext('2d');
- this.width = document.body.clientWidth;
- this.height = document.body.clientHeight;
- this.particles = [];
- },
- createParticles(){
- let dots;
- //ShapeBuilder.write(words, x, y, size)
- ShapeBuilder.write('每个字都是',this.width / 2, this.height / 3, 120);
- dots = ShapeBuilder.getPositions(6);
- ShapeBuilder.write('爱你的模样', this.width / 2, this.height * 2 / 3, 120);
- dots = dots.concat(ShapeBuilder.getPositions(6));
- //dots 已经获取到了字的坐标点
- // 每一个微粒的目标地点都是 dots 的坐标
- // 每一个微粒都随机出生在画布的某个位置
- for(let i = 0; i < dots.length; i++){
- this.particles.push(new Particle({
- xEnd:dots[i].x,
- yEnd:dots[i].y ,
- x: Math.random() * this.width,
- y: Math.random() * this.height,
- size:6,
- color:'hsla(360, 90%, 65%, 1)'
- }));
- }
- },
- loop(){
- // 每一帧清除画布, 然后再渲染微粒就可以了
- requestAnimationFrame(this.loop.bind(this));
- this.ctx.clearRect(0, 0, this.width, this.height);
- for(var i = 0; i < this.particles.length; i++){
- this.particles[i].render(this.ctx);
- }
- }
- }
- canvas.init();
如果想要给每个粒子加上小尾巴的话, 那么在每一帧的时候, 就不要清除画布, 而且覆盖一层有透明度的底色
- // 修改 loop 方法
- //this.ctx.clearRect(0, 0, this.width, this.height);
- this.ctx.fillStyle = 'rgba(0,0,0,0.2)';
- this.ctx.fillRect(0, 0, this.width, this.height);
这样的话会变成如下效果
最后
在这这篇文章的时候, 并没有注意太多细节, 比如 gap 应该是可以被设置的, 或者是一个被特殊标注的常量, 而不应该随便写在方法中对于本例的代码, 切勿生搬硬套, 重要的是要理解原理, 以及自己亲自动手尝试
我也是在写这篇文章的过程中, 才发现了之前获取 position 一个不精准的地方
这里只讲了粒子效果最基础的用法, 实际上还可以做出很多非常炫酷的效果
比如在粒子到达目的地后还可以抖动什么的
粒子形状颜色的变化等等
这个项目还可以搞很多事情的, 大家也可以自己多来尝试弄些更加炫酷的效果
烟花效果可以看一下我的上一篇, 程序员的小浪漫 ---- 烟火
完整项目
github 项目地址
如果觉得还不错, 请 star 一个吧
参考项目
github 上的一个项目 ---- shape-shifter
这个项目我觉得非常不错, 可惜作者都消失好多年了
codepen.io 上的一个作品 ---- Love In Hearts
来源: https://segmentfault.com/a/1190000013379704