Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

文字粒子效果教程 #3

Open
NewNewKing opened this issue Feb 26, 2018 · 0 comments
Open

文字粒子效果教程 #3

NewNewKing opened this issue Feb 26, 2018 · 0 comments

Comments

@NewNewKing
Copy link
Owner

预览

完整项目预览----预览地址;

粒子效果原理

在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值。
  • widthimageData对象的宽。
  • heightimageData对象的高。

首先在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给整除,不然会造成获取坐标点错位的后果。

关于canvasmiddlecenter的规则:

this.ctx.font = 'bold 40px Arial';
this.ctx.fillText('你好',40 ,20);

效果如下图所示

fillText设置的坐标点刚好会是整个字的中点,就是图中middlecenter的交点。其实以其它对齐方式也是可以的,看个人喜好。

更多的对齐规则参考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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant