Canvas绘制星光闪烁的生日祝福

描述

7月7日是个人生日,在期待生日到来的这些天,我用canvas给本身画了生日祝福。以为效果还不错,因此分享一下实现的原理。javascript

整个画面分为两部分,一部分是气球,一部分是生日祝福语,气球和生日祝福语都是以动画的方式逐渐显现出来的。java

演示git

实现

绘制星星汇聚为文字

首先是星星组成的文字,星星是从散乱的各个位置逐步运动到相应的位置组成祝福语的,初始位置比较简单,随机n个点就能够了:github

const randomStartPoints = [];
for(let i = 0;i< texts.length * 100; i++) {
    randomStartPoints.push({
        x: random(0, CANVAS_WIDTH),
        y: random(0, CANVAS_HEIGHT)
    });
}
复制代码

把初始的点放到一个数组中,每一个文字用100个星星来组成。random是lodash的函数。canvas

结束位置由于没啥规律,因此用表的方式来存储下来,好比拿“光”来举栗子:数组

'光': [
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 0, 1, 0, 1, 0, 0, 0],
    [0, 0, 1, 0, 1, 0, 1, 0, 0, 0],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
    [0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
    [0, 0, 0, 1, 0, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 0, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 0, 1, 0, 1, 0, 0],
    [0, 1, 1, 1, 0, 1, 1, 1, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
],
复制代码

我把用到的“光”“生”“日”“快”“乐”这几个字的位置都用这种方式存储起来了。浏览器

以后用它们来生成结束文字的点:bash

const endPoints = [];
texts.forEach((text, index) => {
    const lines =  dotText[text]
    const translateX = GRID_SIZE * 10 * index;

    for(let i = 0; i < lines.length; i ++) {
        for(let j = 0; j < lines[i].length; j ++) {
            if(lines[i][j]) {
                endPoints.push({
                    x: translateX + j * GRID_SIZE,
                    y: i * GRID_SIZE
                });
            }
        }
    }
})
复制代码

texts为那几个文字的数组,GRID_SIZE是每一个单元格的大小,由于文字要排成一列,因此每一个文字都要根据index来计算translateX。app

起点和终点都有了,剩下的就是动画的信息了。dom

const animInfos = [];
let currentIndex = 0;
randomStartPoints.forEach(({x, y}) => {
    const { x: endX, y: endY } = endPoints[currentIndex];
    animInfos.push({
        from: { x, y },
        to: { x: endX, y: endY },
        current: { x, y },
        speed: { x: (endX - x) / ANIM_TIMES, y: (endY - y) / ANIM_TIMES }
    });
    currentIndex = (currentIndex + 1) % endPoints.length;
});
复制代码

循环startPoints的数组,把每个点分配相应的结束点,循环分配的,每一个文字100个星星。而后动画的每一个点的from、to、current就有了,speed是使用运动距离 / 运动次数来算的,运动次数ANIM_TIEMS是一个常量,设置为了20。

动画的信息有了,接下来就是动以及绘制。

设置了ANIM_TIEMS次运动完,每次新位置的计算间隔ANIM_INTERVAl毫秒,我设置为了100。使用定时器没100ms计算一次新位置。

current.x = current.x + speed.x;
current.y = current.y + speed.y;
复制代码

而后运动到结束位置以后,我但愿星星依然有微小的运动,

speed.x = -speed.x;
speed.y = -speed.y;
current.x = current.x + speed.x;
current.y = current.y + speed.y;
复制代码

判断到达结束位置是使用差值来算的,当current和to这俩点的位置偏差小于ERROR_RANGE时,就标记为达到了目标位置,而后记一个flag。

Math.abs(current.x - to.x) <= ERROR_RANGE && Math.abs(current.y - to.y) <= ERROR_RANGE
复制代码

计算部分的总体代码以下:

const animEndFlags = [];
setInterval(() => {
    animInfos.forEach(({ to, current, speed }, index) => {
        if (animEndFlags[index]) {
            speed.x = -speed.x;
            speed.y = -speed.y;
        } else if ( Math.abs(current.x - to.x) <= ERROR_RANGE && Math.abs(current.y - to.y) <= ERROR_RANGE ) {
            speed.x = random(1, -1);
            speed.y = random(1, -1);
            animEndFlags[index] = true;
        }
        current.x = current.x + speed.x;
        current.y = current.y + speed.y;
    });
}, ANIM_INTERVAL);
复制代码

在计算的同时要进行绘制,绘制相关的定时器用requestAnimationFrame,这个会根据浏览器的帧率来自动调节执行频率。

const renderAll = () => {
    ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    animInfos.forEach(({ current }) => {
        ctx.drawImage(icon, current.x, current.y, GRID_SIZE, GRID_SIZE); 
    });
    requestAnimationFrame(renderAll);
}
requestAnimationFrame(renderAll);
复制代码

其中icon也是用canvas来绘制的,就是4条线😂,横、竖、左斜、右斜。

drawStar(ctx, scale, color = '#ffd700') {
    ctx.save();
    ctx.clearRect(0, 0, 100, 100);
    ctx.strokeStyle = color;
    ctx.scale(scale, scale);
    ctx.beginPath();
    ctx.moveTo(0, 10);
    ctx.lineTo(20, 10);
    ctx.moveTo(10, 0);
    ctx.lineTo(10, 20);
    ctx.moveTo(5, 5);
    ctx.lineTo(15, 15);   
    ctx.moveTo(5, 15);
    ctx.lineTo(15, 5);
    ctx.closePath();
    ctx.stroke();
    ctx.restore();
}
复制代码

为了有种闪烁的感受,星星是不断改变大小的:

const repaintIcon = () => {
    this.drawStar(ctxStar, random(0.9, 1.0));
    setTimeout( repaintIcon,random(200, 800));
}
setTimeout(repaintIcon, 100);
复制代码

emmm.如今群星汇聚为祝福语的效果就实现了,撒花~~

绘制气球花边

接下来就要画气球了,其实思路也差很少啦,位置能够动态算出来,我偷了了一个懒,也用的打表的方式。

'~': [
    [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
    [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
    [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0]
  ]
复制代码
const balloonPoints = []
const balloonLines = dotText['~'];
for(var j = 0; j< balloonLines[0].length; j ++) {
    for(var i = 0; i < balloonLines.length; i ++ ){
        if(balloonLines[i][j]) {
            balloonPoints.push( { x:j * GRID_SIZE, y:i * GRID_SIZE} );
        }
    }
}
复制代码

但这里也不是一会儿所有绘制出来的,作成了从左到右依次绘制的效果。就是一个绘制完隔一段时间绘制下一个,我取名叫renderBalloonOneByOne,其中BALLOON_PAINT_INTERVAL常量就是绘制的时间间隔。

const renderBalloonOneByOne = (curIndex) => {
    if( balloonPoints[curIndex]) {
        const { x, y } = balloonPoints[curIndex];
        this.drawBalloon(ctxBalloons2, x, y, random(0, 255), random(0, 255), random(0, 255));

        setTimeout(() => {
            curIndex += 1;
            renderBalloonOneByOne(curIndex);
        }, BALLOON_PAINT_INTERVAL);
    }
}
renderBalloonOneByOne(0);
复制代码

而后气球每一个气球固然也是本身画的啦,此次方式和画星星还不一样,星星是如今canvas上绘制出来,而后把这个canvas用drawImage绘制到目标位置。这里换了一种方式,直接在目标位置绘制的。这样绘制的时候就要计算x和y了。

drawBalloon(ctx, x, y, r, g, b){
    var gradient=ctx.createRadialGradient(x + GRID_SIZE / 3, y + GRID_SIZE / 3, 0, x + GRID_SIZE / 2, y + GRID_SIZE / 2, GRID_SIZE);
    gradient.addColorStop(0,"rgba(255, 255, 255, 1)");            
    gradient.addColorStop(0.4,`rgba(${r}, ${g}, ${b}, 1)`);
    ctx.fillStyle= gradient;
    ctx.beginPath();
    ctx.arc(x + GRID_SIZE / 2, y + GRID_SIZE / 2, GRID_SIZE / 2, 0, Math.PI * 2 );
    ctx.closePath();
    ctx.fill();
}
复制代码

由于颜色随机,因此传入了r、g、b,首先用arc画一个圆,而后填充为一个渐变色,圆心在1/3的左上角,从透明度0到透明度0.4渐变。beginPath和closePath必定都要有,否则会连成一片的。

emmm..气球画的我以为还挺像的。

所用的canvas

星星、气球、文字、气球花边的绘制逻辑就是这样的,不过是在若干个canvas上,文字是独立的一个,星星也是,而后气球直接绘制在目标位置因此也是一个,但上下都有就两个了。因此canvas有这些:

<div class="dazzle-text">
    <!---->
    <canvas ref="canBalloons1" width=1220 height=60></canvas>
    <canvas ref="can" width=1200 height=200></canvas>
    <canvas ref="canBalloons2" width=1220 height=60></canvas>
    <!--star-->
    <canvas class="hide-icon" ref="canStar" width=20 height=20></canvas>
</div>
复制代码

其余

7月7日,也就是今天就是个人生日啦,玩去了~~

源码放下面了,须要的话自取~

源码连接

happy-birthday-guangguang

相关文章
相关标签/搜索