Canvas繪製星光閃爍的生日祝福

一個人的光發表於2019-07-06

描述

7月7日是我的生日,在期待生日到來的這些天,我用canvas給自己畫了生日祝福。覺得效果還不錯,所以分享一下實現的原理。

整個畫面分為兩部分,一部分是氣球,一部分是生日祝福語,氣球和生日祝福語都是以動畫的方式逐漸顯現出來的。

Canvas繪製星光閃爍的生日祝福

實現

繪製星星匯聚為文字

首先是星星組成的文字,星星是從散亂的各個位置逐步運動到相應的位置組成祝福語的,初始位置比較簡單,隨機n個點就可以了:

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的函式。

結束位置因為沒啥規律,所以用表的方式來儲存下來,比如拿“光”來舉栗子:

'光': [
    [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]
],
複製程式碼

我把用到的“光”“生”“日”“快”“樂”這幾個字的位置都用這種方式儲存起來了。

之後用它們來生成結束文字的點:

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。

起點和終點都有了,剩下的就是動畫的資訊了。

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

相關文章